This commit is contained in:
juyoungk09 2025-09-12 07:03:07 +09:00
commit ba3b85d622
12 changed files with 683 additions and 310 deletions

View file

@ -5,7 +5,7 @@ import { getCookie } from "~/utils/cookie";
export const BuyModal = component$(({ showBuyModal, item, mydotory, itemdotory}: {showBuyModal: Signal<boolean>, item: any, mydotory: number, itemdotory: number}) => {
const handleBuy = $(async() => {
try {
const res = await axios.post(`http://localhost:8000/api/store/${item.id}?product_name=${item.name}`, {
const res = await axios.post(`http://localhost:8000/api/store/0?product_name=${item.name}`, {
}, {
headers: {
Authorization: `Bearer ${getCookie("access_token")}`,

View file

View file

@ -16,7 +16,7 @@ export default component$(({ furniture, avatar, roomType }: { furniture: Signal<
top_clothe_type: "",
bottom_clothe_type: ""
});
const furnitureItems = useSignal<FurnitureItem[]>([]);
useVisibleTask$(({ track }) => {
track(() => avatar.value);
console.log(avatar.value);
@ -25,43 +25,44 @@ export default component$(({ furniture, avatar, roomType }: { furniture: Signal<
avatar_type: typechecker(avatar.value.avatar_type),
top_clothe_type: typechecker(avatar.value.top_clothe_type),
bottom_clothe_type: typechecker(avatar.value.bottom_clothe_type),
};
};
furnitureItems.value = [furniture.value];
console.log(checkedAvatar.value);
})
const furnitureItems = useSignal<FurnitureItem[]>([
{
name: '큰 식물1',
image_path: 'public/funiture/큰 식물.png',
x: 1,
y: 1
},
{
name: '녹색 침대1',
image_path: 'public/funiture/녹색 침대-90.png',
x: 3,
y: 3
},
{
name: '쓰레기통열림1',
image_path: 'public/funiture/쓰레기통열림.png',
x: 5,
y: 5
},
{
name: '어항1',
image_path: 'public/funiture/어항-0.png',
x: 7,
y: 7
}
]);
// const furnitureItems = useSignal<FurnitureItem[]>([
// {
// name: '큰 식물1',
// image_path: 'public/funiture/큰 식물.png',
// x: 1,
// y: 1
// },
// {
// name: '녹색 침대1',
// image_path: 'public/funiture/녹색 침대-90.png',
// x: 3,
// y: 3
// },
// {
// name: '쓰레기통열림1',
// image_path: 'public/funiture/쓰레기통열림.png',
// x: 5,
// y: 5
// },
// {
// name: '어항2',
// image_path: 'public/funiture/어항-0.png',
// x: 7,
// y: 7
// }
// ]);
// 현재 드래그 중인 가구
const draggedItem = useSignal<FurnitureItem | null>(null);
// 드래그 시작 시 호출
const handleDragStart = $((item: FurnitureItem, e: Event) => {
e.preventDefault();
draggedItem.value = item;
});
// const draggedItem = useSignal<FurnitureItem | null>(null);
// // 드래그 시작 시 호출
// const handleDragStart = $((item: FurnitureItem, e: Event) => {
// e.preventDefault();
// draggedItem.value = item;
// });
// 드래그 오버 시 기본 동작 방지
const handleDragOver = $((e: Event) => {
@ -72,29 +73,35 @@ export default component$(({ furniture, avatar, roomType }: { furniture: Signal<
const handleCellClick = $((x: number, y: number, e: Event) => {
e.preventDefault();
console.log(`Cell clicked: (${x}, ${y})`);
furnitureItems.value = furnitureItems.value.map(item =>
item.name === furniture.value?.name
? { ...item, x: x, y: y }
: item
);
// draggedItem.value = null;
});
// 드롭 시 호출
const handleDrop = $((cellX: number, cellY: number, e: Event) => {
e.preventDefault();
if (!draggedItem.value) return;
// const handleDrop = $((cellX: number, cellY: number, e: Event) => {
// e.preventDefault();
// if (!draggedItem.value) return;
// 이미 해당 위치에 다른 가구가 있는지 확인
const isOccupied = furnitureItems.value.some(
item => item.x === cellX && item.y === cellY
);
// // 이미 해당 위치에 다른 가구가 있는지 확인
// const isOccupied = furnitureItems.value.some(
// item => item.x === cellX && item.y === cellY
// );
if (!isOccupied) {
// 가구 위치 업데이트
furnitureItems.value = furnitureItems.value.map(item =>
item.name === draggedItem.value?.name
? { ...item, x: cellX, y: cellY }
: item
);
}
// if (!isOccupied) {
// // 가구 위치 업데이트
// furnitureItems.value = furnitureItems.value.map(item =>
// item.name === draggedItem.value?.name
// ? { ...item, x: cellX, y: cellY }
// : item
// );
// }
draggedItem.value = null;
});
// draggedItem.value = null;
// });
// 10x10 그리드 셀 생성
@ -125,40 +132,21 @@ export default component$(({ furniture, avatar, roomType }: { furniture: Signal<
/>
{/* Grid overlay */}
<div class="grid-overlay">
<div class="grid-overlay ">
{gridCells.map((cell) => (
<div
key={`${cell.x}-${cell.y}`}
class="grid-cell"
style={{
gridColumn: cell.x,
gridRow: cell.y,
position: 'relative',
width: '64px',
height: '64px',
// border: '1px dashed rgba(255, 255, 255, 0.1)',
cursor: 'pointer',
backgroundColor: cell.furniture ? 'rgba(0, 0, 0, 0.2)' : 'transparent'
}}
onDragOver$={handleDragOver}
onDrop$={ e => handleDrop(cell.x, cell.y, e)}
// onDragOver$={handleDragOver}
// onDrop$={ e => handleDrop(cell.x, cell.y, e)}
onClick$={ e => handleCellClick(cell.x, cell.y, e)}
>
{cell.furniture && (
<img
src={`http://localhost:8000/${cell.furniture.src}`}
alt={cell.furniture.name}
draggable
onDragStart$={ e => handleDragStart(furnitureItems.value.find(f => f.name === cell.furniture?.name)!, e)}
style={{
position: 'absolute',
width: '100%',
height: '100%',
objectFit: 'contain',
imageRendering: 'pixelated',
cursor: 'move',
zIndex: 10
}}
// draggable
// onDragStart$={ e => handleDragStart(furnitureItems.value.find(f => f.name === cell.furniture?.name)!, e)}
/>
)}
</div>
@ -198,17 +186,6 @@ const STYLES = `
object-fit: cover;
}
.grid-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: grid;
grid-template-columns: repeat(10, 1fr);
grid-template-rows: repeat(10, 1fr);
pointer-events: auto;
}
.avatar-wrapper {
position: absolute;
@ -257,7 +234,7 @@ const STYLES = `
grid-template-columns: repeat(10, 1fr);
grid-template-rows: repeat(10, 1fr);
z-index: 2;
pointer-events: none;
}
.grid-overlay div {
@ -287,7 +264,18 @@ const STYLES = `
transform: translate(-50%, -50%);
z-index: 99;
}
.grid-overlay div#oneone>img {
width: 70px;
image-rendering: pixelated;
position: absolute;
bottom: 0;
}
.grid-overlay div#twoone>img {
width: 140px;
image-rendering: pixelated;
position: absolute;
transform: translate(-50%, -50%);
}
.avatar-wrapper img {
position: absolute;
top: 0;

View file

@ -0,0 +1,140 @@
import { component$, useStylesScoped$ , useVisibleTask$ } from "@builder.io/qwik";
interface FurnitureItem {
image_path: string;
x: number;
y: number;
size?: "one" | "two"; // 크기 구분 (one = 70px, two = 140px)
}
interface AvatarItem {
avatar_type: {name: string, path: string};
top_clothe_type: {name: string, path: string};
bottom_clothe_type: {name: string, path: string};
}
interface ShowRoomGridProps {
roomSrc: string; // 방 배경
furnitures: FurnitureItem[]; // 가구 리스트
avatars: AvatarItem; // 아바타 이미지 리스트
}
export default component$((props: ShowRoomGridProps) => {
console.log(props.roomSrc);
console.log(props.furnitures + " furnitures");
console.log(props.avatars + " avatars");
useStylesScoped$(STYLES);
useVisibleTask$(() => {
});
return (
<div class="room-wrapper">
{/* 배경 */}
<img src={"http://localhost:8000/public/room/"+props.roomSrc+".png"} class="room" />
{/* 10x10 격자 */}
<div class="grid-overlay">
{Array.from({ length: 100 }).map((_, idx) => {
const item = props.furnitures.find((f) => f.x*10 + f.y === idx);
console.log(item);
return (
<div
key={idx}
class={
item?.size === "one"
? "oneone"
: item?.size === "two"
? "twoone"
: undefined
}
style={
item?.size === "two"
? { gridColumn: `span 2`, gridRow: `span 1` }
: undefined
}
>
{item && <img src={"http://localhost:8000/" + item.image_path} />}
</div>
);
})}
</div>
{/* 아바타 */}
<div class="avatar-wrapper">
<img src={"http://localhost:8000/"+props.avatars.avatar_type.path} />
<img src={"http://localhost:8000/"+props.avatars.top_clothe_type.path} />
<img src={"http://localhost:8000/"+props.avatars.bottom_clothe_type.path} />
</div>
</div>
);
});
const STYLES = `
.room-wrapper {
display: flex;
align-items: center;
justify-content: center;
width: 700px;
height: 700px;
position: relative;
}
.room {
position: absolute;
z-index: 1 !important;
width: 100%;
height: 100%;
image-rendering: pixelated;
}
.grid-overlay {
position: absolute;
top: 0;
left: 0;
width: 700px;
height: 700px;
display: grid;
grid-template-columns: repeat(10, 1fr);
grid-template-rows: repeat(10, 1fr);
z-index: 2;
pointer-events: none;
}
.grid-overlay div {
/* border: 1px solid rgba(0,0,0,0.2); */
box-sizing: border-box;
position: relative;
width: 70px;
height: 70px;
position: relative;
}
// .grid-overlay div.oneone > img {
// width: 140px;
// image-rendering: pixelated;
// position: absolute;
// bottom: 0;
// }
// .grid-overlay div.twoone > img {
// width: 140px;
// image-rendering: pixelated;
// position: absolute;
// transform: translate(-50%, -50%);
// }
.avatar-wrapper {
width: 100px;
height: 100px;
position: absolute;
top: 70%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 99;
}
.avatar-wrapper img {
position: absolute;
top: 0;
left: 0;
width: 100%;
image-rendering: pixelated;
z-index: 99;
}
`;

View file

@ -5,7 +5,7 @@ import { DocumentHead } from "@builder.io/qwik-city";
import { useLocation } from "@builder.io/qwik-city";
import axios from "axios";
import { routeLoader$ } from "@builder.io/qwik-city";
export const onRequest: RequestHandler = async ({ cookie, params, sharedMap}) => {
export const onGet: RequestHandler = async ({ cookie, params, sharedMap}) => {
console.log(params.username + "의 상점 페이지로 들어옴");
@ -15,7 +15,7 @@ export const onRequest: RequestHandler = async ({ cookie, params, sharedMap}) =>
Authorization: `Bearer ${cookie.get("access_token")?.value}`,
},
});
sharedMap.set("dotory", res.data ?? 0);
sharedMap.set("dotory", res.data.dotory ?? 0);
console.log(res.data ?? "no data");
} catch (error) {
console.log(error);
@ -29,8 +29,8 @@ export default component$(() => {
const location = useLocation();
const dotory = useDotory();
console.log(location.url.pathname);
console.log("/"+location.params.username+"/store");
// console.log(location.url.pathname);
// console.log(location.params.username+"/store");
return (
<div class="w-full h-full flex flex-col">
<div class="flex justify-center flex-col gap-4 p-2 items-center w-full">
@ -40,8 +40,6 @@ export default component$(() => {
<Link href={`/${location.params.username}/room`} class={`${location.url.pathname === `/${location.params.username}/room` ? "bg-[#FAD659]" : "bg-white"} text-gray-800 px-4 py-2 rounded-lg font-medium`}></Link>
</div>
<div class="flex md:flex-row flex-col">
{/* <div class="w-12 h-12 bg-[#33231A] [clip-path:polygon(20%_0,100%_0,80%_100%,20%_100%,0_80%,0_20%)]">
</div> */}
<div class="ml-auto bg-[#CC8A6A] text-default px-4 py-2 rounded-lg font-medium">: {dotory.value}</div>
</div>
</div>

View file

@ -45,39 +45,18 @@ interface Room {
room_image_path: string;
}
export const useRoomLoader = routeLoader$(async ({cookie}) => {
const headers = {
Authorization: `Bearer ${cookie.get("access_token")?.value}`,
}
try {
const myRoom = await axios.get("http://localhost:8000/api/room/layout", {
headers: {
Authorization: `Bearer ${cookie.get("access_token")?.value}`,
},
})
const myAvatar = await axios.get("http://localhost:8000/api/avatar", {
headers: {
Authorization: `Bearer ${cookie.get("access_token")?.value}`,
},
})
const roomType = await axios.get("http://localhost:8000/api/room/types", {
headers: {
Authorization: `Bearer ${cookie.get("access_token")?.value}`,
},
})
const furniture = await axios.get("http://localhost:8000/api/room/my", {
headers: {
Authorization: `Bearer ${cookie.get("access_token")?.value}`,
},
})
const avatarType = await axios.get("http://localhost:8000/api/avatar/options", {
headers: {
Authorization: `Bearer ${cookie.get("access_token")?.value}`,
},
})
const roomTypeData = roomType.data;
const avatar : Avatar = myAvatar.data;
const room : Room = myRoom.data;
const furnitureData = furniture.data;
const avatarTypeData = avatarType.data;
console.log(myRoom.data);
return {myData : {room: room, avatar: avatar}, selectionData: {roomType: roomTypeData, furniture: furnitureData, avatarType: avatarTypeData}};
const [myRoom, myAvatar, roomType, furniture, avatarType] = await Promise.all([
axios.get("http://localhost:8000/api/room/layout", { headers }),
axios.get("http://localhost:8000/api/avatar", { headers }),
axios.get("http://localhost:8000/api/room/types", { headers }),
axios.get("http://localhost:8000/api/room/my", { headers }),
axios.get("http://localhost:8000/api/avatar/options", { headers }),
]);
return {myData : {room: myRoom.data, avatar: myAvatar.data}, selectionData: {roomType: roomType.data, furniture: furniture.data, avatarType: avatarType.data}};
} catch (error : any) {
console.error(error);
return error.response?.data?.detail || 'Room not found';
@ -85,8 +64,10 @@ export const useRoomLoader = routeLoader$(async ({cookie}) => {
})
export default component$(() => {
const data = useRoomLoader();
const selectedFurniture = useSignal<Furniture[]>([]); // 유저 선택 가구
const selectedType = useSignal("furniture"); // 헤더에서 가구, 아바타, 배경 구분
const selectedFurnitures = useSignal<Array<Furniture & {x?: number, y?: number}>>([]);
const selectedType = useSignal<"furniture" | "avatar" | "background">("furniture");
const editingPosition = useSignal<number | null>(null);
const selectedRoomType = useSignal("room_1");
const selectedAvatar = useSignal<SetAvatarType>({
avatar_type: "",
@ -100,99 +81,31 @@ export default component$(() => {
selectedAvatar.value.bottom_clothe_type = data.value.myData.avatar.bottom_clothe_type.name;
})
const deleteFurniture = $((selectedFurniture : Furniture) => {
const furnitureRes = axios.delete("http://localhost:8000/api/room/furniture", {
data: {
furniture_name: selectedFurniture.furniture_name,
x: selectedFurniture.x,
y: selectedFurniture.y,
},
headers: {
Authorization: `Bearer ${getCookie("access_token")}`,
},
})
})
// const getNewState = async () => {
// const myRoom = await axios.get("http://localhost:8000/api/room/layout", {
// headers: {
// Authorization: `Bearer ${getCookie("access_token")}`,
// },
// })
// const myAvatar = await axios.get("http://localhost:8000/api/avatar", {
// headers: {
// Authorization: `Bearer ${getCookie("access_token")}`,
// },
// })
// const roomType = await axios.get("http://localhost:8000/api/room/types", {
// headers: {
// Authorization: `Bearer ${getCookie("access_token")}`,
// },
// })
// const furniture = await axios.get("http://localhost:8000/api/room/my", {
// headers: {
// Authorization: `Bearer ${getCookie("access_token")}`,
// },
// })
// const avatarType = await axios.get("http://localhost:8000/api/avatar/options", {
// headers: {
// Authorization: `Bearer ${getCookie("access_token")}`,
// },
// })
// const roomTypeData = roomType.data;
// const avatar : Avatar = myAvatar.data.avatar;
// const room : Room = myRoom.data;
// const furnitureData = furniture.data;
// const avatarTypeData = avatarType.data;
// console.log(myRoom.data);
// }
const handleChange = $( async () => {
console.log("handleChange");
const deleteFurniture = $(async (selectedFurniture : Furniture) => {
try {
// 배경 설정
console.log(selectedRoomType.value);
const roomRes = await axios.patch("http://localhost:8000/api/room/",
{
type: selectedRoomType.value
},
{
headers: {
Authorization: `Bearer ${getCookie("access_token")}`,
},
}
);
const avatarRes = await axios.put("http://localhost:8000/api/avatar", {
avatar_type: selectedAvatar.value.avatar_type,
top_clothe_type: selectedAvatar.value.top_clothe_type,
bottom_clothe_type: selectedAvatar.value.bottom_clothe_type,
},
{
headers: {
Authorization: `Bearer ${getCookie("access_token")}`,
},
}
);
// for (let i = 0; i < selectedFurniture.value.length; i++) {
// const furnitureRes = await axios.post("http://localhost:8000/api/room/furniture", {
// furniture_name: selectedFurniture.value[i].furniture_name,
// x: selectedFurniture.value[i].x,
// y: selectedFurniture.value[i].y,
// },
// {
// headers: {
// Authorization: `Bearer ${getCookie("access_token")}`,
// },
// })
// }
// getNewState();
console.log(roomRes.data);
console.log(avatarRes.data);
} catch (error) {
console.log(error);
}
await axios.delete(`http://localhost:8000/api/room/furniture?x=${selectedFurniture.x}&y=${selectedFurniture.y}&furniture_name=${selectedFurniture.furniture_name}`, {headers: {Authorization: `Bearer ${getCookie("access_token")}`}});
selectedFurnitures.value = selectedFurnitures.value.filter((f : Furniture) => f.furniture_name !== selectedFurniture.furniture_name);
} catch (error : any) {console.error(error);}
})
const handleCancel = $(() => {
console.log("handleCancel");
const addFurniture = $(async (selectedFurniture : Furniture) => {
try { await axios.post("http://localhost:8000/api/room/furniture", {furniture_name: selectedFurniture.furniture_name,x: selectedFurniture.x,y: selectedFurniture.y,}, { headers: { Authorization: `Bearer ${getCookie("access_token")}`,},},)
} catch (error : any) {console.error(error);}
})
useTask$(({track}) => {
track(() => selectedFurnitures.value);
try {
for (let i = 0; i < selectedFurnitures.value.length; i++) {
if(data.value.myData.room.room.furniture.some((f : Furniture) => f.furniture_name === selectedFurnitures.value[i].furniture_name)) {deleteFurniture(selectedFurnitures.value[i]);}
else { addFurniture(selectedFurnitures.value[i]);}
}
} catch (error : any) {console.error(error);}
})
const handleChange = $( async () => {
try {
await axios.patch("http://localhost:8000/api/room/", {type: selectedRoomType.value}, {headers: {Authorization: `Bearer ${getCookie("access_token")}`}});
await axios.put("http://localhost:8000/api/avatar", {avatar_type: selectedAvatar.value.avatar_type,top_clothe_type: selectedAvatar.value.top_clothe_type,bottom_clothe_type: selectedAvatar.value.bottom_clothe_type,}, {headers: {Authorization: `Bearer ${getCookie("access_token")}`}});
} catch (error) {console.log(error);}
})
return (
<div class="flex flex-col h-full p-4">
@ -201,7 +114,7 @@ export default component$(() => {
{/* Room Preview */}
<div class="flex-1 bg-white rounded-xl border border-gray-200 p-4">
<div class="h-full flex items-center justify-center">
<RoomGrid furniture={selectedFurniture} avatar={selectedAvatar} roomType={selectedRoomType} />
<RoomGrid furniture={selectedFurnitures} avatar={selectedAvatar} roomType={selectedRoomType} />
</div>
</div>
@ -251,12 +164,81 @@ export default component$(() => {
<div class="flex-1 overflow-y-auto p-4">
<div class="flex flex-col gap-3">
{selectedType.value === "furniture" ? (
data.value.selectionData.furniture.map((furniture: Furniture) => (
<div key={furniture.furniture_name} class={`flex items-center flex-col gap-2 rounded-t-lg` } onClick$={() => {if(selectedFurniture.value.includes(furniture)) selectedFurniture.value = selectedFurniture.value.filter((item: Furniture) => item !== furniture); else selectedFurniture.value.push(furniture);}}>
<img src={`http://localhost:8000/${furniture.image_path}`} alt={furniture.furniture_name} style={{imageRendering: "pixelated"}} class={` object-contain w-24 h-24 ${selectedFurniture.value.includes(furniture) ? "border-[2px] border-[#FAD659]" : ""}`} />
<span class="text-center font-bold rounded-b-lg w-24 h-12 bg-[#E5E2B6]">{furniture.furniture_name}</span>
</div>
)))
data.value.selectionData.furniture.map((furniture: Furniture) => {
const isSelected = selectedFurnitures.value.some(f => f.furniture_name === furniture.furniture_name);
const selectedItem = selectedFurnitures.value.find(f => f.furniture_name === furniture.furniture_name);
return (
<div key={furniture.furniture_name} class="flex flex-col items-center gap-2">
<div
class={`flex flex-col items-center gap-2 rounded-t-lg ${isSelected ? 'ring-2 ring-yellow-400' : ''}`}
onClick$={() => {
if (isSelected) {
selectedFurnitures.value = selectedFurnitures.value.filter(
item => item.furniture_name !== furniture.furniture_name
);
} else {
selectedFurnitures.value = [...selectedFurnitures.value, {
...furniture,
x: 0,
y: 0
}];
}
}}
>
<img
src={`http://localhost:8000/${furniture.image_path}`}
alt={furniture.furniture_name}
style={{imageRendering: "pixelated"}}
class="object-contain w-24 h-24"
/>
<span class="text-center font-bold rounded-b-lg w-24 h-12 bg-[#E5E2B6] flex items-center justify-center">
{furniture.furniture_name}
</span>
</div>
{isSelected && (
<div class="flex gap-2 mt-1">
<input
type="number"
min="1"
max="10"
value={selectedItem?.x || ''}
onInput$={(e) => {
const value = (e.target as HTMLInputElement).value;
const index = selectedFurnitures.value.findIndex(f => f.furniture_name === furniture.furniture_name);
if (index !== -1) {
const updated = [...selectedFurnitures.value];
updated[index] = { ...updated[index], x: parseInt(value) || 0 };
selectedFurnitures.value = updated;
}
}}
class="w-12 px-1 text-center border rounded"
placeholder="X"
/>
<input
type="number"
min="1"
max="10"
value={selectedItem?.y || ''}
onInput$={(e) => {
const value = (e.target as HTMLInputElement).value;
const index = selectedFurnitures.value.findIndex(f => f.furniture_name === furniture.furniture_name);
if (index !== -1) {
const updated = [...selectedFurnitures.value];
updated[index] = { ...updated[index], y: parseInt(value) || 0 };
selectedFurnitures.value = updated;
}
}}
class="w-12 px-1 text-center border rounded"
placeholder="Y"
/>
</div>
)}
</div>
);
}
))
:
selectedType.value === "avatar" ? (
<div class="flex flex-col gap-3 ">

View file

@ -34,6 +34,16 @@ interface ShopResponse {
export const useShopLoader = routeLoader$(async ({cookie}) => {
try {
// const getDotory = await axios.put(`http://localhost:8000/api/store?dotory_num=100`, {
// headers: {
// Authorization: `Bearer ${cookie.get("access_token")?.value}`,
// },
// // });
// const dotory = await axios.get(`http://localhost:8000/api/store`, {
// headers: {
// Authorization: `Bearer ${cookie.get("access_token")?.value}`,
// },
// });
const res = await axios.get(`http://localhost:8000/api/room/catalog`, {
headers: {
Authorization: `Bearer ${cookie.get("access_token")?.value}`,
@ -51,14 +61,15 @@ export const useShopLoader = routeLoader$(async ({cookie}) => {
// });
console.log(res.data);
console.log(avatar.data);
return {storeData: res.data, avatarData: avatar.data, dotory: 0};
return {storeData: res.data, avatarData: avatar.data, dotory: 80};
} catch (error : any) {
console.error(error);
return { storeData: [], avatarData: null, dotory: 0};
}
});
export default component$(() => {
const data = useShopLoader() as { value: ShopResponse };
const data = useShopLoader();
const selectedItem = useSignal<ShopItem | null>(null);
const showBuyModal = useSignal(false);
return (
@ -84,7 +95,7 @@ export default component$(() => {
class="bg-diary-color hover:bg-diary-icon-hover md:w-full text-black px-4 py-2 rounded-3xl font-bold transition-all duration-200 flex items-center gap-2 z-10 relative"
onClick$={() => {showBuyModal.value = true}}>
<i class="bi bi-cart-fill text-3xl text-black"></i>
<i class="bi bi-cart text-3xl text-black"></i>
</button>
</div>
}

View file

@ -1,44 +1,272 @@
import { component$, useSignal } from "@builder.io/qwik";
import { DocumentHead , RequestHandler, routeLoader$} from "@builder.io/qwik-city";
import { useNavigate } from "@builder.io/qwik-city";
import { $, component$, useSignal, useVisibleTask$, NoSerialize, useTask$ } from "@builder.io/qwik";
import { routeLoader$, useLocation, useNavigate } from "@builder.io/qwik-city";
import axios from "axios";
const diaryLoader = routeLoader$<{ message: string }>(async ({params, cookie, redirect}) => {
try {
console.log(params.username + "의 방명록 가져오기");
const myInfo = await axios.get(`http://localhost:8000/api/user/me`, {
headers: {
Authorization: `Bearer ${cookie.get("access_token")?.value}`,
},
});
if(myInfo.data.username !== params.username) {
return redirect(302, `/diary/${params.diary_id}`);
}
const res = await axios.get(`http://localhost:8000/api/diary/${params.diary_id}`);
console.log(res.data);
return res.data;
} catch (error : any) {
console.error(error);
return error.response?.data?.detail || 'Diary not found';
import { noSerialize } from "@builder.io/qwik";
// import { PhotoUploadModal } from "~/components/features/diary/UploadImages";
import { getCookie } from "~/utils/cookie";
interface FileInfo {
name: string;
size: number;
type: string;
lastModified: number;
url: string;
}
interface DiaryData {
content: string;
title: string;
category: string;
images: File[];
}
export const diaryDataLoader = routeLoader$(async ({params, cookie}) => {
const access_token = cookie.get("access_token");
const diary_id = params.diary_id;
const diaryData = await axios.get(`http://localhost:8000/api/diary/${diary_id}`, {
headers: {
Authorization: `Bearer ${access_token?.value}`,
},
});
return diaryData.data;
})
export default component$(() => {
const diaryData = diaryDataLoader();
const content = useSignal('');
const title = useSignal('');
const category = useSignal('');
const selectedImages = useSignal<File[]>([] );
const textareaRef = useSignal<HTMLTextAreaElement>();
const nav = useNavigate();
const location = useLocation();
const showPhotoUploadModal = useSignal(false);
const fileInfo = useSignal<FileInfo | null>(null);
const fileRef = useSignal<NoSerialize<File> | null>(null);
const error = useSignal('');
// const fileInputRef = useSignal<HTMLInputElement>();
useTask$(() => {
content.value = diaryData.value.content;
title.value = diaryData.value.title;
category.value = diaryData.value.category;
selectedImages.value = diaryData.value.images;
})
const handleFileSelect = $(async (selectedFile: File) => {
if (selectedFile && selectedFile.type.startsWith('image/')) {
fileInfo.value = {
name: selectedFile.name,
size: selectedFile.size,
type: selectedFile.type,
lastModified: selectedFile.lastModified,
url: URL.createObjectURL(selectedFile)
};
fileRef.value = noSerialize(selectedFile);
error.value = '';
console.log(fileInfo.value);
} else {
error.value = '이미지 파일만 업로드 가능합니다.';
fileInfo.value = null;
fileRef.value = null;
}
});
export default component$(() => {
const navigate = useNavigate();
const diaryData = diaryLoader();
return (
<div>
<h1> </h1>
</div>
);
});
export const head: DocumentHead = {
title: "일기수정하기",
meta: [
{
name: "description",
content: "유저가 쓴 일기의 상세정보",
const handleRemoveImage = $((index: number) => {
selectedImages.value = selectedImages.value.filter((_, i) => i !== index);
});
const handleSend = $(async () => {
try {
const formData = new FormData();
formData.append('title', title.value);
formData.append('content', content.value);
formData.append('category', category.value);
// Append each image file
selectedImages.value.forEach((file) => {
formData.append('file', file);
});
await axios.put(`http://localhost:8000/api/diary/${location.params.diary_id}`, formData, {
headers: {
'Content-Type': 'multipart/form-data',
Authorization: `Bearer ${getCookie("access_token")}`,
},
],
};
});
nav(`/${location.params.username}/diary`);
} catch (error: any) {
console.error('Error creating diary:', error);
}
});
useVisibleTask$(({ track }) => {
track(() => content.value);
if (textareaRef.value) {
textareaRef.value.style.height = 'auto';
textareaRef.value.style.height = `${textareaRef.value.scrollHeight}px`;
}
});
return (
<div class="p-6">
<div class="flex justify-between items-center mb-8">
<h1 class="text-2xl font-bold"> </h1>
</div>
<div class="border-b-2 border-text-default flex flex-row justify-between items-center">
<input
bind:value={title}
type="text"
placeholder="제목을 입력해주세요."
class="w-full text-2xl font-bold p-2 focus:outline-none"
/>
<div class="flex gap-3">
<button onClick$={() => nav(`/${location.params.username}/diary`)}>
<i class="bi bi-trash-fill text-3xl text-black"></i>
</button>
<button onClick$={() => handleSend()}>
<i class="bi bi-send-fill text-3xl text-black"></i>
</button>
</div>
</div>
{/* Image upload and preview section */}
<div class="p-4">
<div class="flex gap-2 overflow-x-auto py-2">
{selectedImages.value.map((image, index) => (
<div key={index} class="relative">
<img
src={URL.createObjectURL(image)}
alt={`Preview ${index}`}
class="h-24 w-24 object-cover rounded"
/>
<button
onClick$={() => handleRemoveImage(index)}
class="absolute -top-2 -right-2 bg-red-500 text-white rounded-full w-6 h-6 flex items-center justify-center"
>
×
</button>
</div>
))}
<button
onClick$={() => showPhotoUploadModal.value = true}
>
<i class="bi bi-image-fill text-black text-2xl"></i>
</button>
</div>
</div>
<div class="relative">
<textarea
ref={textareaRef}
bind:value={content}
placeholder="본문을 입력해주세요"
class="w-full min-h-[70vh] p-4 bg-white border border-text-default rounded-lg focus:outline-none resize-none leading-relaxed whitespace-pre-wrap"
style={{
backgroundImage: 'linear-gradient(#f1f1f1 1px, transparent 1px)',
backgroundSize: '100% 1.5rem',
lineHeight: '1.5rem',
backgroundAttachment: 'local',
backgroundPositionY: '0.75rem',
}}
/>
</div>
<div >
{selectedImages.value.map((file, index) => (
<div key={index}>
<img src={URL.createObjectURL(file)} alt={`Preview ${index}`} />
</div>
))}
</div>
{showPhotoUploadModal.value &&
<div class="fixed inset-0 bg-black bg-opacity-45 flex items-center justify-center z-50" onClick$={() => showPhotoUploadModal.value = false}>
<div class="bg-white rounded-lg w-[400px] p-6 shadow-xl relative" onClick$={(e) => e.stopPropagation()}>
<button
class="absolute top-4 right-4 text-xl text-black"
onClick$={() => showPhotoUploadModal.value = false}
aria-label="닫기"
>
<i class="bi bi-x-lg"></i>
</button>
<h2 class="text-3xl font-bold text-center text-black"> </h2>
<p class="text-xs p-2 text-gray-500 text-center border-b-2 border-black mb-6">
</p>
<div
class={`border-2 border-dashed rounded-md h-32 flex flex-col items-center justify-center text-sm cursor-pointer transition-colors ${
fileInfo.value
? 'border-green-300 bg-green-50 text-green-700'
: 'border-gray-300 bg-gray-100 text-gray-500 hover:border-blue-400'
}`}
onDragOver$={(e) => e.preventDefault()}
onDrop$={async (e) => {
e.preventDefault();
const files = e.dataTransfer?.files;
if (files && files.length > 0) {
await handleFileSelect(files[0]);
}
}}
onClick$={async () => {
if (fileRef.value) {
try {
console.log('업로드됨:', {
file: fileRef.value,
fileInfo: fileInfo.value
});
// 업로드 후 초기화
fileInfo.value = null;
fileRef.value = null;
showPhotoUploadModal.value = false;
} catch (err) {
console.error('업로드 실패:', err);
error.value = '파일 업로드 중 오류가 발생했습니다.';
}
}
}}
>
{fileInfo.value ? (
<div class="text-center p-4">
<i class="bi bi-check-circle-fill text-2xl mb-1"></i>
<p class="font-medium">{fileInfo.value.name}</p>
<p class="text-xs">{(fileInfo.value.size / 1024).toFixed(1)} KB</p>
</div>
) : (
<div class="text-center p-4">
<i class="bi bi-image-fill text-2xl mb-1"></i>
<p> </p>
<p class="text-xs mt-1">JPG, PNG, GIF</p>
</div>
)}
</div>
{error.value && (
<p class="text-red-500 text-sm mt-2">{error.value}</p>
)}
<button
class="mt-6 w-full bg-[#FFAD84] hover:bg-[#ff9e6b] text-[#4b3c28] font-semibold py-2 rounded-md flex justify-center items-center gap-1 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
disabled={!fileInfo.value}
onClick$={async () => {
if (fileRef.value) {
try {
console.log('Uploading:', {
file: fileRef.value,
fileInfo: fileInfo.value
});
selectedImages.value = [...selectedImages.value, fileRef.value];
fileInfo.value = null;
fileRef.value = null;
} catch (err) {
console.error('업로드 실패:', err);
error.value = '파일 업로드 중 오류가 발생했습니다.';
}
}
}}
>
<i class="bi bi-cloud-upload"></i>
</button>
</div>
</div>
}
</div>
);
});

View file

@ -74,7 +74,7 @@ export default component$(() => {
</div>
<div class="mt-8 flex flex-col gap-4">
<div class="flex gap-4 overflow-auto">
<div class="flex gap-4 overflow-auto border-t-[1px] border-b-[1px] border-text-default p-2">
{diaryLoader.value.diary.images.map((image, index) => (
<img key={index} src={`http://localhost:8000/${image}`} alt={`일기 ${index + 1}`} class="max-w-full h-auto max-h-64" />
))}

View file

@ -1,4 +1,4 @@
import { component$, useSignal, $, useTask$, useContext } from "@builder.io/qwik";
import { component$, useSignal, $, useVisibleTask$, useContext } from "@builder.io/qwik";
import type { DocumentHead } from "@builder.io/qwik-city";
import { useNavigate, useLocation, Link, routeLoader$ } from "@builder.io/qwik-city";
import axios from "axios";
@ -7,7 +7,7 @@ export const useDiaryLoader = routeLoader$( async ({cookie, params}) => {
try {
const userid = await axios.get(`http://localhost:8000/api/user/profile/${params.username}`);
console.log(userid.data.id + "<- 이건 아이디");
const res = await axios.get(`http://localhost:8000/api/diary/w/{user_id}?target_user_id=${userid.data.id}&skip=0&limit=6`, {
const res = await axios.get(`http://localhost:8000/api/diary/w/${userid.data.id}?skip=0&limit=6`, {
headers: {
Authorization: `Bearer ${cookie.get("access_token")?.value}`,
},
@ -25,7 +25,7 @@ export default component$(() => {
const entryLoader = useDiaryLoader();
const entries = useSignal<DiaryEntry[]>([]);
const filteredEntries = useSignal<DiaryEntry[]>([]);
useTask$(() => {
useVisibleTask$(() => {
console.log( entryLoader.value.user);
entries.value = entryLoader.value.diaries as DiaryEntry[];
filteredEntries.value = entries.value;
@ -39,21 +39,21 @@ export default component$(() => {
});
const handleMonthSelect = $((month : number) => {
selectedMonth.value = month;
console.log(selectedMonth.value);
filteredEntries.value = entries.value.filter((entry) => {
const entryMonth = new Date(entry.created_at).getMonth() + 1;
return entryMonth === selectedMonth.value;
});
console.log(filteredEntries.value);
console.log("<->" + selectedMonth.value);
showMonth.value = false;
});
const handleSearch = $(() => {
if(searchQuery.value === '') {
filteredEntries.value = entries.value;
filteredEntries.value = [...entries.value];
}
else {
filteredEntries.value = entries.value.filter((entry) => entry.title.includes(searchQuery.value));
filteredEntries.value = [...entries.value.filter((entry) => entry.title.includes(searchQuery.value))];
}
console.log(filteredEntries.value);
});

View file

@ -1,4 +1,4 @@
import { component$, useSignal, $, useTask$} from "@builder.io/qwik";
import { component$, useSignal, $, useVisibleTask$} from "@builder.io/qwik";
import type { DocumentHead } from "@builder.io/qwik-city";
import { routeLoader$, useLocation } from "@builder.io/qwik-city";
import { getCookie } from "~/utils/cookie";
@ -70,18 +70,25 @@ export default component$(() => {
const selectedMessage = useSignal<GuestbookContent>({} as GuestbookContent);
const updatedMessage = useSignal('');
const refreshMessages = $(async () => {
try {
const res = await axios.get<GuestbookContent[]>(`http://localhost:8000/api/guestbook/${loaderData.value.targetUser.id}`);
messages.value = res.data;
console.log(messages.value + " loader");
} catch (error) {
console.log("방명록 가져오기 실패" +loaderData.value.targetUser.id);
}
});
useTask$(() => {
useVisibleTask$(() => {
console.log("useTask");
refreshMessages();
messages.value = loaderData.value.messages;
});
const handleSubmit = $(() => {
const handleSubmit = $(async () => {
if (!newMessage.value.trim()) return;
try {
axios.post(`http://localhost:8000/api/guestbook/`, {
console.log(newMessage.value);
await axios.post(`http://localhost:8000/api/guestbook`, {
content: newMessage.value,
target_user_id: loaderData.value.targetUser.id,
}, {
@ -96,16 +103,20 @@ export default component$(() => {
}
newMessage.value = '';
});
const handleEdit = $(() => {
const handleEdit = $(async () => {
console.log("수정 요청 보내기");
if (!updatedMessage.value.trim()) {
console.log("수정 내용이 없습니다.");
return;
}
try {
axios.put(`http://localhost:8000/api/guestbook/${selectedMessage.value.id}`, {
await axios.put(`http://localhost:8000/api/guestbook/${selectedMessage.value.id}`, {
content: updatedMessage.value,
}, {
headers: {
Authorization: `Bearer ${getCookie("access_token")}`,
},
});
refreshMessages();
} catch (error) {
console.error(error);
@ -154,9 +165,11 @@ export default component$(() => {
</div>
}
{showUpdateModal.value && msg.id === selectedMessage.value.id && (
<div
<form
class="absolute z-50 bg-white right-0 top-0 p-2 rounded-xl focus:outline-none w-full min-w-[220px]"
onClick$={(e) => e.stopPropagation()} /* 외부 클릭으로 닫히는 건 부모가 처리하게 둘 수 있음 */
// onClick$={(e) => e.stopPropagation()} /* 외부 클릭으로 닫히는 건 부모가 처리하게 둘 수 있음 */
preventdefault:submit
onSubmit$={handleEdit}
>
<div class="flex mt-2 justify-between">
<p> .</p>
@ -164,7 +177,7 @@ export default component$(() => {
<button class="btn-cancel text-button-color-1 border-black" onClick$={() => showUpdateModal.value = false}>
<i class="bi bi-x"></i>
</button>
<button class="btn-save text-diary-color border-black" onClick$={handleEdit}>
<button class="btn-save text-diary-color border-black">
<i class="bi bi-check"></i>
</button>
</div>
@ -174,7 +187,7 @@ export default component$(() => {
onInput$={(ev: any) => (updatedMessage.value = ev.target.value)}
class="w-full p-2 border border-input-border-color rounded-xl"
/>
</div>
</form>
)}
</div>
))}
@ -191,7 +204,7 @@ export default component$(() => {
{/* <button class="absolute right-12 w-6 h-6 top-1/2 rounded-full bg-gray-500 p-0.5 transform -translate-x-1/2 -translate-y-1/2 text-center">
<i class="bi bi-images text-white text-center"></i>
</button> */}
<button onclick$={handleSubmit} class="absolute right-4 w-6 h-6 top-1/2 rounded-full bg-gray-500 p-0.5 transform -translate-x-1/2 -translate-y-1/2 text-center">
<button class="absolute right-4 w-6 h-6 top-1/2 rounded-full bg-gray-500 p-0.5 transform -translate-x-1/2 -translate-y-1/2 text-center">
<i class="bi bi-send text-white text-center"></i>
</button>
</form>

View file

@ -1,10 +1,10 @@
import { component$, useContext, useVisibleTask$ } from "@builder.io/qwik";
import { type DocumentHead, routeLoader$, Link, useLocation } from "@builder.io/qwik-city";
import axios from "axios";
import RoomGrid from "~/components/room/RoomGrid";
import { useContextProvider, useSignal, $ } from "@builder.io/qwik";
import { useSignal, $ } from "@builder.io/qwik";
import { AppStateContext } from "~/utils/context";
import { getCookie } from "~/utils/cookie";
import ShowRoomGrid from "~/components/room/ShowRoomGrid";
interface FriendRequest {
id: number;
user_id: number;
@ -13,42 +13,50 @@ interface FriendRequest {
status: string;
created_at: string;
}
interface User {
id: number;
username: string;
email: string;
created_at: string;
profile_image_path: string;
is_active: boolean;
}
export const useDataLoader = routeLoader$(async ({params, cookie}) => {
const headers = {
Authorization: `Bearer ${cookie.get("access_token")?.value}`,
}
try {
// console.log(cookie.get("access_token")?.value + "이것은 토큰");
const userid = await axios.get(`http://localhost:8000/api/user/profile/${params.username}`);
// console.log(userid.data.id + "<- 이것은 아이디");
const res = await axios.get(`http://localhost:8000/api/diary/w/{userid.data.id}?target_user_id=${userid.data.id}&skip=0&limit=6`, {
headers: {
Authorization: `Bearer ${cookie.get("access_token")?.value}`,
},
});
const friendRequest = await axios.get(`http://localhost:8000/api/friendship/pending`, {
headers: {
Authorization: `Bearer ${cookie.get("access_token")?.value}`,
},
});
const room = await axios.get(`http://localhost:8000/api/room/layout`, {
headers: {
Authorization: `Bearer ${cookie.get("access_token")?.value}`,
},
});
const avatar = await axios.get(`http://localhost:8000/api/avatar`, {
headers: {
Authorization: `Bearer ${cookie.get("access_token")?.value}`,
},
});
const user = await axios.get<User>(`http://localhost:8000/api/user/profile/${params.username}`);
const [room, avatar, friendRequest, diaries] = await Promise.all([
axios.get(`http://localhost:8000/api/room/layout/${user.data.id}`, { headers }),
axios.get(`http://localhost:8000/api/avatar/${user.data.id}`, { headers }),
axios.get(`http://localhost:8000/api/friendship/pending`, { headers }),
axios.get(`http://localhost:8000/api/diary/w/${user.data.id}?skip=0&limit=6`, {
headers: {
Authorization: `Bearer ${cookie.get("access_token")?.value}`,
},
}),
]);
// const avatar = await axios.get(`http://localhost:8000/api/avatar/${userid.data.id}`, {
// headers: {
// Authorization: `Bearer ${cookie.get("access_token")?.value}`,
// },
// });
// console.log(res.data+ "다이어리 데이터 불러오기");
// console.log("??/" + res.data.diaries);
return {diaries: res.data, friendRequest: friendRequest.data as FriendRequest[], userdata: {room: room.data, avatar: avatar.data} };
return {diaries: diaries.data, friendRequest: friendRequest.data as FriendRequest[], userdata: {room: room.data, avatar: avatar.data} };
} catch (error : any) {
console.error(error);
console.error("에러 : " + error.config.url);
return {diaries: [], friendRequest: [] as FriendRequest[], userdata: {room: [], avatar: [] } };
}
});
export default component$(() => {
const data = useDataLoader();
console.log(data.value.userdata.avatar);
const showMyFriendsRequest = useSignal<boolean>(false);
const userData = useContext(AppStateContext);
const selectedRoom = useSignal({});
@ -90,6 +98,11 @@ export default component$(() => {
<div>
<div class="flex flex-col lg:flex-row gap-8">
{/* <RoomGrid furniture={data.value.room} avatar={data.value.avatar} roomType={data.value.roomType} /> */}
<ShowRoomGrid
roomSrc={data.value.userdata.room.room.room_type}
furnitures={data.value.userdata.room.furniture}
avatars={data.value.userdata.avatar}
/>
<section class="mb-6 flex flex-col justify-between gap-8">
<div class="flex-1">
<h2 class="text-xl w-full pl-3 pb-1 font-bold border-b-2 border-[#4b3c28] inline-block mb-2">Update</h2>