This commit is contained in:
juyoungk09 2025-09-10 17:31:47 +09:00
commit afe581ec34
69 changed files with 12702 additions and 25 deletions

20
Dockerfile Normal file
View file

@ -0,0 +1,20 @@
FROM node:20-alpine AS builder
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:20-alpine
WORKDIR /app
COPY --from=builder /app .
EXPOSE 5173
CMD ["npm", "run", "start"]

9801
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -26,16 +26,25 @@
"@builder.io/qwik": "^1.15.0",
"@builder.io/qwik-city": "^1.15.0",
"@eslint/js": "latest",
"@types/jsonwebtoken": "^9.0.10",
"@types/node": "20.14.11",
"autoprefixer": "^10.4.21",
"eslint": "9.25.1",
"eslint-plugin-qwik": "^1.15.0",
"globals": "16.0.0",
"postcss": "^8.5.6",
"prettier": "3.3.3",
"tailwindcss": "^3.4.17",
"typescript": "5.4.5",
"typescript-eslint": "8.26.1",
"typescript-plugin-css-modules": "latest",
"undici": "*",
"vite": "5.3.5",
"vite-tsconfig-paths": "^4.2.1"
},
"dependencies": {
"axios": "^1.11.0",
"bootstrap-icons": "^1.13.1",
"jsonwebtoken": "^9.0.2"
}
}

11
postcss.config.js Normal file
View file

@ -0,0 +1,11 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}
// const config = {
// plugins: ["@tailwindcss/postcss"],
// };
// export default config;

View file

@ -0,0 +1,93 @@
Copyright 2024 The Wanted Sans Project Authors (https://github.com/wanteddev/wanted-sans)
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
https://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.

View file

@ -0,0 +1,57 @@
/*
Copyright (c) 2024 Wanted Lab, Inc., with Reserved Font Name Wanted Sans.
https://github.com/wanteddev/wanted-sans
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
*/
@font-face {
font-family: "Wanted Sans";
font-style: normal;
font-display: swap;
font-weight: 1000;
src: local("Wanted Sans ExtraBlack"), url("./woff2/WantedSans-ExtraBlack.woff2") format("woff2");
}
@font-face {
font-family: "Wanted Sans";
font-style: normal;
font-display: swap;
font-weight: 900;
src: local("Wanted Sans Black"), url("./woff2/WantedSans-Black.woff2") format("woff2");
}
@font-face {
font-family: "Wanted Sans";
font-style: normal;
font-display: swap;
font-weight: 800;
src: local("Wanted Sans ExtraBold"), url("./woff2/WantedSans-ExtraBold.woff2") format("woff2");
}
@font-face {
font-family: "Wanted Sans";
font-style: normal;
font-display: swap;
font-weight: 700;
src: local("Wanted Sans Bold"), url("./woff2/WantedSans-Bold.woff2") format("woff2");
}
@font-face {
font-family: "Wanted Sans";
font-style: normal;
font-display: swap;
font-weight: 600;
src: local("Wanted Sans SemiBold"), url("./woff2/WantedSans-SemiBold.woff2") format("woff2");
}
@font-face {
font-family: "Wanted Sans";
font-style: normal;
font-display: swap;
font-weight: 500;
src: local("Wanted Sans Medium"), url("./woff2/WantedSans-Medium.woff2") format("woff2");
}
@font-face {
font-family: "Wanted Sans";
font-style: normal;
font-display: swap;
font-weight: 400;
src: local("Wanted Sans Regular"), url("./woff2/WantedSans-Regular.woff2") format("woff2");
}

View file

@ -0,0 +1,15 @@
/*
Copyright (c) 2024 Wanted Lab, Inc., with Reserved Font Name Wanted Sans Variable.
https://github.com/wanteddev/wanted-sans
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
*/
@font-face {
font-family: "Wanted Sans Variable";
font-style: normal;
font-display: swap;
font-weight: 400 1000;
src: url("./woff2/WantedSansVariable.woff2") format("woff2-variations");
}

View file

@ -0,0 +1,4 @@
<svg width="23" height="26" viewBox="0 0 23 26" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10.5944 0V12.6875H12.4069V0H10.5944Z" fill="black"/>
<path d="M2.43815 14.1593C2.43791 12.5407 2.87115 10.9516 3.69286 9.55706C4.51458 8.16257 5.69477 7.0136 7.11078 6.22956L6.23172 4.64544C4.13017 5.80918 2.47309 7.63602 1.51917 9.84075C0.565252 12.0455 0.368182 14.504 0.958726 16.8326C1.54927 19.1611 2.89419 21.2285 4.78353 22.7122C6.67287 24.1958 9.00028 25.0122 11.4024 25.0337C13.8046 25.0553 16.1463 24.2808 18.0619 22.8313C19.9776 21.3818 21.3594 19.3388 21.9916 17.0213C22.6238 14.7037 22.4709 12.242 21.5567 10.0205C20.6425 7.79904 19.0185 5.94277 16.9382 4.7415L16.0301 6.30931C17.408 7.10443 18.5523 8.24826 19.3481 9.62582C20.1438 11.0034 20.5629 12.5661 20.5632 14.157C20.5635 15.7479 20.145 17.3108 19.3498 18.6887C18.5546 20.0665 17.4107 21.2108 16.033 22.0064C14.6554 22.802 13.0926 23.221 11.5018 23.2212C9.91091 23.2214 8.34802 22.8028 6.9702 22.0075C5.59239 21.2122 4.44821 20.0683 3.65266 18.6906C2.85712 17.3129 2.43825 15.7501 2.43815 14.1593Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View file

@ -0,0 +1,121 @@
import { component$, Signal, useSignal, $, useTask$, } from "@builder.io/qwik";
import axios from "axios";
import { getCookie } from "~/utils/cookie";
export const AddFriends = component$(({ showAddFriends }: { showAddFriends: Signal<boolean> }) => {
const friends = useSignal([]) as Signal<any[]>;
const myfriends = useSignal([]) as Signal<any[]>;
const searchQuery = useSignal('');
useTask$(() => {
try {
axios.get(`http://localhost:8000/api/friendship`, {
headers: {
Authorization: `Bearer ${getCookie("access_token")}`,
},
})
.then((res) => {
myfriends.value = res.data;
})
} catch (error) {
console.log(error);
}
})
const handleAddFriend = $((username: string) => {
try {
axios.post(`http://localhost:8000/api/friendship/request`, {
friend_username: username,
}, {
headers: {
Authorization: `Bearer ${getCookie("access_token")}`,
},
})
.then((res) => {
})
} catch (error : any) {
console.log(error.response?.data?.detail);
}
})
const handleSearch = $((query: string) => {
try {
axios.get(`http://localhost:8000/api/user/find/${query}`, {
})
.then((res) => {
friends.value = res.data;
})
} catch (error : any) {
console.log(error);
}
})
const handleDeleteFriend = $((friendship_id : number) => {
try {
axios.delete(`http://localhost:8000/api/friendship/${friendship_id}`, {
headers: {
Authorization: `Bearer ${getCookie("access_token")}`,
},
})
.then((res) => {
console.log(res.data);
myfriends.value = myfriends.value.filter((friend) => friend.id !== friendship_id);
})
} catch (error : any) {
console.log(error.response?.data?.detail);
}
})
return (
<div onclick$={() => {showAddFriends.value = false; console.log(showAddFriends.value)}} class="bg-opacity-45 z-[1000] fixed inset-0 w-full h-full py-6 px-6 bg-black">
<div class="flex w-full h-full justify-center items-center">
<div onclick$={(e) => {e.stopPropagation()}} class="max-w-[1001px] w-full bg-white rounded-lg shadow p-4">
{/* 헤더 */}
<div class="flex justify-between items-center mb-3 border-b pb-2 border-black">
<h1 class="text-3xl font-bold"> </h1>
<button onclick$={() => {showAddFriends.value = false; console.log(showAddFriends.value)}} class="text-black text-xl"><i class="bi bi-x-square"></i></button>
</div>
{/* 검색창 */}
<form class="relative" preventdefault:submit onSubmit$={() => {handleSearch(searchQuery.value)}}>
<input
type="text"
placeholder="닉네임/아이디 검색"
class="w-full border border-black text-text-default rounded px-10 py-2 focus:outline-none focus:ring-1 focus:ring-[#4b3c28]"
bind:value={searchQuery}
/>
<button class="absolute left-2 top-2 text-black hover:text-gray-600 text-xl"><i class="bi bi-search"></i></button>
</form>
{/* 검색 결과 */}
<p class="text-lg font-bold p-2"> {friends.value.length}</p>
<div class="divide-y divide-gray-200 max-h-64 overflow-y-auto">
{friends.value.map((friend, i) => (
<div key={i} class="flex items-center justify-between py-3">
<div class="flex items-center gap-3">
<img class={`w-8 h-8 rounded-full`} src={`http://localhost:8000/${friend.profile_image_path}`} />
<div class="flex flex-col">
<span class="font-medium text-sm">{friend.username}</span>
</div>
</div>
<button class="text-black hover:text-red-500" onclick$={() => {handleAddFriend(friend.username)}}><i class="bi bi-plus"></i></button>
</div>
))}
</div>
<div class="w-full border-b-2 border-y-2 p-1 flex justify-between border-gray-400">
<p class="text-lg font-bold"> </p>
{/* <i class="bi bi-arrow-clockwise"></i> */}
</div>
{/* 이미 추가된 친구들 */}
<div class="divide-y divide-gray-200 max-h-64 overflow-y-auto">
{myfriends.value.map((friend, i) => (
<div key={i} class="flex items-center justify-between py-3">
<div class="flex items-center gap-3">
{/* <img class={`w-8 h-8 rounded-full`} src={`http://localhost:8000/${friend.profile_image_path}`} /> */}
<div class="flex flex-col">
<span class="font-medium text-sm">{friend.friend_username}</span>
<span class="text-xs text-gray-500">{friend.friend_id}</span>
</div>
</div>
<button class="text-black hover:text-red-500" onclick$={() => {handleDeleteFriend(friend.id)}}><i class="bi bi-trash"></i></button>
</div>
))}
</div>
</div></div>
</div>
);
});

View file

@ -0,0 +1,10 @@
import { component$ } from "@builder.io/qwik";
export const NoticeBoard = component$(() => {
return (
<div>
<h1>Update</h1>
</div>
);
});

View file

@ -0,0 +1,43 @@
// src/components/PhotoUploadModal.tsx
import { component$, useSignal, Signal, $ } from '@builder.io/qwik';
type FileInfo = {
name: string;
size: number;
type: string;
lastModified: number;
url?: string;
};
export const PhotoUploadModal = component$(({ selectedImages, showPhotoUploadModal }: { selectedImages: Signal<File[]>, showPhotoUploadModal: Signal<boolean> }) => {
const fileInfo = useSignal<FileInfo | null>(null);
const fileRef = useSignal<File | null>(null);
const error = useSignal('');
const fileInputRef = useSignal<HTMLInputElement>();
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 = selectedFile;
error.value = '';
} else {
error.value = '이미지 파일만 업로드 가능합니다.';
fileInfo.value = null;
fileRef.value = null;
}
});
if (!showPhotoUploadModal.value) return null;
return (
);
});

View file

@ -0,0 +1,29 @@
import { $, component$, Signal } from "@builder.io/qwik";
import axios from "axios";
import { getCookie } from "~/utils/cookie";
export default component$(( { messages, showDeleteModal, selectedMessage }: { messages: Signal<any>, showDeleteModal: Signal<boolean>, selectedMessage: Signal<any> }) => {
const deleteMessage = $((id: number) => {
axios.delete(`http://localhost:8000/api/guestbook/${id}`, {
headers: {
Authorization: `Bearer ${getCookie("access_token")}`,
},
});
messages.value = messages.value.filter((message: any) => message.id !== id);
});
return (
<div class="bg-opacity-45 z-[1000] fixed inset-0 w-full h-full py-6 px-6 bg-black flex items-center justify-center" onClick$={() => {showDeleteModal.value = false; selectedMessage.value = null}}>
<div class="bg-white p-4 rounded-lg flex gap-2 flex-col items-center justify-center" onClick$={(e) => e.stopPropagation()}>
<p>?</p>
<div class="flex gap-2">
<button class="bg-button-color-1 text-black px-4 py-2 rounded-lg" onclick$={() => {
showDeleteModal.value = false;
deleteMessage(selectedMessage.value.id)}}></button>
<button class="bg-button-color-3 text-black px-4 py-2 rounded-lg" onclick$={() => {
showDeleteModal.value = false;
selectedMessage.value = null}}></button>
</div>
</div>
</div>
);
});

View file

@ -0,0 +1,44 @@
import { component$, Signal, $ } from "@builder.io/qwik";
import axios from "axios";
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}`, {
}, {
headers: {
Authorization: `Bearer ${getCookie("access_token")}`,
},
})
console.log(res.data);
showBuyModal.value = false;
} catch (error) {
console.log(error);
}
});
const handleCancel = $(() => {
showBuyModal.value = false;
});
return (
<div class="flex h-full w-full items-center justify-center bg-black bg-opacity-35">
<div class="flex flex-col items-center gap-2 rounded-lg bg-white p-10">
<p> {item.name} </p>
<p class="font-bold text-2xl"> ?</p>
<p class="text-gray-500"> : {mydotory - itemdotory}</p>
<div class="flex gap-2">
<button onclick$={handleBuy} class="bg-diary-color hover:bg-diary-icon-hover rounded-lg p-2">
<i class="bi bi-cart-plus"></i>
</button>
<button onclick$={handleCancel} class="bg-button-color-2 hover:bg-button-color-2-hover rounded-lg p-2">
<i class="bi bi-x"></i>
</button>
</div>
</div>
</div>
);
});

View file

@ -0,0 +1,12 @@
import { component$ } from "@builder.io/qwik";
export const ItemCard = component$(({item}: {item: any}) => {
return (
<div>
<img src={`http://localhost:8000/${item.image_path}`} alt={item.name} />
<h1>{item.name}</h1>
<p>{item.price}</p>
</div>
);
});

View file

@ -0,0 +1,116 @@
import { $, component$, Signal, useSignal, useVisibleTask$ } from "@builder.io/qwik";
import terminate from "~/assets/images/terminate.svg";
import { useNavigate, useLocation } from "@builder.io/qwik-city";
import { Link } from "@builder.io/qwik-city";
import { deleteCookie, getCookie } from "~/utils/cookie";
import { AddFriends } from "~/components/features/AddFriends";
import axios from "axios";
export default component$(({ showAddFriends }: { showAddFriends: Signal<boolean> }) => {
const navigate = useNavigate();
const location = useLocation();
const handleLogout = $(() => {
console.log("logout");
deleteCookie("access_token");
navigate("/auth");
});
const myInfo = useSignal<Friend>({
id: 0,
username: "",
email: "",
created_at: "",
profile_image_path: "",
is_active: false
});
const visitedUser = useSignal<Friend>({
id: 0,
username: "",
email: "",
created_at: "",
profile_image_path: "",
is_active: false
});
useVisibleTask$(() => {
try {
axios.get('http://localhost:8000/api/user/me', {
headers: {
Authorization: `Bearer ${getCookie("access_token")}`,
},
}).then((res) => {
myInfo.value = res.data;
console.log(myInfo.value);
})
.catch((error) => {
console.log(error);
})
axios.get(`http://localhost:8000/api/user/profile/${location.params.username}`, {
headers: {
Authorization: `Bearer ${getCookie("access_token")}`,
},
}).then((res) => {
console.log(res.data);
visitedUser.value = res.data;
})
.catch((error) => {
console.log(error);
})
} catch (error) {
console.log(error);
}
})
const nickname = location.params.username;
return (
<header class="sticky xl:flex hidden left-0 top-0 min-h-screen w-64 p-4 bg-custom-gradient flex-col pt-16 overflow-y-auto">
{visitedUser.value.id !== myInfo.value.id &&
<a href={`/${myInfo.value.username}/`} class="text-default w-full text-center underline mb-2">
</a>
}
<div class="flex flex-col items-center gap-2">
<div class="w-48 h-48 hidden md:block">
<input type="image" src={`http://localhost:8000/${visitedUser.value.profile_image_path}`} class=" object-cover w-full h-full bg-white rounded-full" alt="profile" />
</div>
<p class="text-gray-500 text-sm max-w-64 w-full bg-white pl-2 p-1 rounded-md">{nickname}</p>
</div>
<h1 class="mt-8 text-2xl font-bold border-t-2 border-black pt-2 pb-6 hidden sm:block"></h1>
<nav class="">
<ul class="space-y-2 text-xl">
<li >
<Link href={`/${nickname}/`} class="flex items-center p-3 rounded-lg hover:bg-white hover:text-black transition-colors">
<i class="bi bi-house-door"></i>
<span class="ml-2 hidden sm:block"></span>
</Link>
</li>
<li>
<Link href={`/${nickname}/diary`} class="flex items-center p-3 rounded-lg hover:bg-white hover:text-black transition-colors">
<i class="bi bi-journal-bookmark"></i>
<span class="ml-2 hidden sm:block"></span>
</Link>
</li>
<li>
<Link href={`/${nickname}/guestbook`} class="flex items-center p-3 rounded-lg hover:bg-white hover:text-black transition-colors">
<i class="bi bi-journals"></i>
<span class="ml-2 hidden sm:block"></span>
</Link>
</li>
<li onclick$={() => {showAddFriends.value = true; console.log(showAddFriends.value)}} class={myInfo.value.username === visitedUser.value.username ? "" : "hidden"}>
<div class="flex hover:cursor-pointer items-center p-3 rounded-lg hover:bg-white hover:text-black transition-colors">
<i class="bi bi-person-add"></i>
<span class="ml-2 hidden sm:block"> </span>
</div>
</li>
<li class={myInfo.value.username === visitedUser.value.username ? "" : "hidden"}>
<Link href={`/${nickname}/store`} class="flex items-center p-3 rounded-lg hover:bg-white hover:text-black transition-colors">
<i class="bi bi-bag"></i>
<span class="ml-2 hidden sm:block"></span>
</Link>
</li>
</ul>
</nav>
<div class="mt-auto flex flex-row justify-center items-center gap-2">
<p class="text-sm text-center text-black border-b-2 border-black"></p>
<button class="h-4 w-4" onClick$={handleLogout}><img src={terminate} width={12} height={12} alt="terminate" /></button>
</div>
</header>
);
});

View file

@ -0,0 +1,299 @@
import { $, component$, useSignal, useStylesScoped$, Signal, useVisibleTask$} from "@builder.io/qwik";
import { typechecker } from "~/utils/func";
interface FurnitureItem {
name: string;
image_path: string;
x: number;
y: number;
}
export default component$(({ furniture, avatar, roomType }: { furniture: Signal<any>, avatar: Signal<any>, roomType: Signal<string> }) => {
useStylesScoped$(STYLES);
// 가구 아이템 상태 관리
const checkedAvatar = useSignal({
avatar_type: "",
top_clothe_type: "",
bottom_clothe_type: ""
});
useVisibleTask$(({ track }) => {
track(() => avatar.value);
console.log(avatar.value);
checkedAvatar.value = {
...avatar.value,
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),
};
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 draggedItem = useSignal<FurnitureItem | null>(null);
// 드래그 시작 시 호출
const handleDragStart = $((item: FurnitureItem, e: Event) => {
e.preventDefault();
draggedItem.value = item;
});
// 드래그 오버 시 기본 동작 방지
const handleDragOver = $((e: Event) => {
e.preventDefault();
});
// 셀 클릭 시 호출
const handleCellClick = $((x: number, y: number, e: Event) => {
e.preventDefault();
console.log(`Cell clicked: (${x}, ${y})`);
});
// 드롭 시 호출
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
);
if (!isOccupied) {
// 가구 위치 업데이트
furnitureItems.value = furnitureItems.value.map(item =>
item.name === draggedItem.value?.name
? { ...item, x: cellX, y: cellY }
: item
);
}
draggedItem.value = null;
});
// 10x10 그리드 셀 생성
const gridCells = Array.from({ length: 100 }, (_, i) => {
const row = Math.floor(i / 10) + 1; // 1-10
const col = (i % 10) + 1; // 1-10
// 해당 위치의 가구 찾기
const furniture = furnitureItems.value.find(item => item.x === col && item.y === row);
return {
x: col,
y: row,
furniture: furniture ? {
name: furniture.name,
src: furniture.image_path,
} : null
};
});
return (
<div class="room-wrapper">
<img
src={`http://localhost:8000/public/room/${roomType.value}.png`}
class="room"
alt="Room background"
/>
{/* 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)}
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
}}
/>
)}
</div>
))}
</div>
{/* Avatar */}
<div class="avatar-wrapper">
<img
src={`http://localhost:8000/${checkedAvatar.value.avatar_type}`}
alt={checkedAvatar.value.avatar_type}
/>
<img
src={`http://localhost:8000/${checkedAvatar.value.top_clothe_type}`}
alt={checkedAvatar.value.top_clothe_type}
/>
<img
src={`http://localhost:8000/${checkedAvatar.value.bottom_clothe_type}`}
alt={checkedAvatar.value.bottom_clothe_type}
/>
</div>
</div>
);
});
const STYLES = `
.room-wrapper {
position: relative;
width: 100%;
height: 100%;
overflow: hidden;
}
.room {
width: 100%;
height: 100%;
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;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
pointer-events: none;
}
.avatar-wrapper img {
position: absolute;
top: 0;
left: 0;
width: 75px;
height: 100px;
object-fit: contain;
image-rendering: pixelated;
}
.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;
border-radius: 0.5rem;
}
/* Grid */
.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 {
box-sizing: border-box;
position: relative;
}
.grid-overlay > div {
position: relative;
height: 70px;
width: 100%;
overflow: visible;
}
.grid-overlay > div img {
pointer-events: none;
user-select: none;
-webkit-user-drag: none;
}
.avatar-wrapper {
width: 200px;
height: 200px;
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

View file

@ -0,0 +1,19 @@
@font-face {
font-family: "Wanted Sans";
src: url("./assets/fonts/WantedSans-1.0.3/webfonts/static/complete/WantedSans.css");
}
* {
font-family: "Wanted Sans";
box-sizing: border-box;
margin: 0;
padding: 0;
}
html, body {
/* height: 100%; */
}
input:-webkit-autofill {
-webkit-box-shadow: 0 0 0px 1000px white inset !important;
-webkit-text-fill-color: #000 !important;
transition: background-color 5000s ease-in-out 0s;
}

View file

@ -1,8 +1,9 @@
import { component$, isDev } from "@builder.io/qwik";
import { QwikCityProvider, RouterOutlet } from "@builder.io/qwik-city";
import { RouterHead } from "./components/router-head/router-head";
import './tailwind.css';
import "./global.css";
import "bootstrap-icons/font/bootstrap-icons.css";
export default component$(() => {
/**

View file

@ -0,0 +1,54 @@
@keyframes fadeIn {
from { opacity: 0; transform: translateX(-10px); }
to { opacity: 1; transform: translateX(0); }
}
@keyframes slideInRight {
from {
opacity: 0;
transform: translateX(20px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
.animate-fadeIn {
animation: fadeIn 0.4s ease-out forwards;
}
.animate-slideInRight {
animation: slideInRight 0.4s ease-out forwards;
}
.form-container {
@apply h-full transition-all duration-500 ease-in-out;
}
.form-initial {
@apply w-full max-w-4xl p-8 bg-white lg:rounded-tl-[150px] lg:rounded-br-[150px] rounded-bl-md rounded-tr-md shadow-[0_12px_4px_-2px_rgba(0,0,0,0.3)];
}
.form-expanded {
@apply w-full max-w-7xl flex overflow-hidden bg-white lg:rounded-tl-[150px] lg:rounded-br-[150px] rounded-bl-md rounded-tr-md shadow-[0_12px_4px_-2px_rgba(0,0,0,0.3)];
}
.greeting-side {
@apply flex-1 p-12 flex flex-col justify-center items-center text-center;
}
.form-side {
@apply p-4 flex flex-col justify-center;
}
@media (max-width: 425px) {
.form-expanded {
@apply flex-col;
}
.greeting-side,
.form-side {
@apply p-4;
}
}

View file

@ -0,0 +1,180 @@
import { $, component$, useSignal, useStore, noSerialize , type NoSerialize } from "@builder.io/qwik";
import type { DocumentHead } from "@builder.io/qwik-city";
import "./auth.css";
import axios from "axios";
import { useNavigate } from "@builder.io/qwik-city";
import { setCookie } from "~/utils/cookie";
export default component$(() => {
const navigate = useNavigate();
const isRegistering = useSignal(false);
const userState = useStore({
username: '',
password: '',
error: '',
email: '',
profile_file: null as NoSerialize<File> | null,
});
const fileInputRef = useSignal<HTMLInputElement>();
const handleFileInput = $((event: Event) => {
const file = (event.target as HTMLInputElement).files?.[0] ?? null;
userState.profile_file = file ? noSerialize(file) : null;
});
const handleSubmit = $(async () => {
console.log(userState);
if (isRegistering.value) {
try {
console.log('Register start');
const formData = new FormData();
if (!userState.username.trim()) {
userState.error = "이름을 입력해주세요.";
return;
}
if (!userState.password.trim()) {
userState.error = "비밀번호를 입력해주세요.";
return;
}
if (!userState.email.trim()) {
userState.error = "이메일을 입력해주세요.";
return;
}
formData.append('username', userState.username.trim());
formData.append('password', userState.password.trim());
formData.append('email', userState.email.trim());
if (userState.profile_file) {
formData.append('profile_file', userState.profile_file);
}
console.log(formData);
const res = await axios.post('http://localhost:8000/api/user/register', formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
});
console.log(res.data);
console.log('Register success');
userState.error = '';
userState.username = '';
userState.password = '';
userState.email = '';
userState.profile_file = null;
isRegistering.value = false;
} catch (error : any) {
console.error('Register failed:', error);
userState.error = error.response?.data?.detail || 'Register failed';
}
} else {
try {
console.log('Login start');
if (!userState.username.trim()) {
userState.error = "이름을 입력해주세요.";
return;
}
if (!userState.password.trim()) {
userState.error = "비밀번호를 입력해주세요.";
return;
}
console.log(userState.username.trim(), userState.password.trim());
const res = await axios.post('http://localhost:8000/api/user/login', {
username: userState.username.trim(),
password: userState.password.trim(),
})
console.log('Login success');
userState.username = '';
userState.password = '';
userState.error = '';
userState.email = '';
userState.profile_file = null;
setCookie('access_token', res.data.access_token);
isRegistering.value = false;
navigate('/');
} catch (error : any) {
console.error('Login failed:', error);
userState.error = error.response?.data?.detail || 'Login failed';
}
}
});
return (
<div class="flex items-center w-full h-full justify-center min-h-screen">
<div class={`form-container flex h-full min-h-[720px] justify-center ${isRegistering.value ? 'form-expanded' : 'form-initial'} shadow-[0_6px_12px_-2px_rgba(0,0,0, 0.2)]`}>
{isRegistering.value ? (
// 회원가입
<div class="w-full flex flex-col lg:flex-row">
<div class="greeting-side">
<h1 class="md:text-4xl text-3xl font-bold mb-4"></h1>
<p class="mb-3 text-xs font-bold text-gray-custom opacity-70"> , .</p>
<i class="bi bi-people h-auto leading-none bg-clip-text text-transparent bg-signup-icon md:text-[15rem] text-[10rem]"></i>
</div>
<div class="form-side flex-auto ">
<form class="space-y-4" onSubmit$={handleSubmit} preventdefault:submit>
<div >
<label for="email" class="block text-sm font-medium text-default"> <i class="bi bi-envelope text-input-icon-color"></i></label>
<input id="email" onInput$={(e) => userState.email = (e.target as HTMLInputElement).value} type="email" class="w-full p-3 border border-input-border-color rounded-xl" placeholder="이메일 입력" />
</div>
<div>
<label for="username" class="block text-sm font-medium text-default"> <i class="bi bi-person text-input-icon-color"></i></label>
<input id="username" onInput$={(e) => userState.username = (e.target as HTMLInputElement).value} type="text" class="w-full p-3 border border-input-border-color rounded-xl" placeholder="이름 입력" />
</div>
<div>
<label for="password" class="block text-sm font-medium text-default"> <i class="bi bi-lock text-input-icon-color"></i></label>
<input id="password" autocomplete="new-password" onInput$={(e) => userState.password = (e.target as HTMLInputElement).value} type="password" class="w-full p-3 border border-input-border-color rounded-xl" placeholder="8~20자 영문 대소문자, 숫자, 특수문자 비밀번호 입력" />
</div>
<div>
<label for="profileImage" class="block text-sm font-medium text-default"> <i class="bi bi-image text-input-icon-color"></i></label>
<input id="profileImage" type="file" ref={fileInputRef} onChange$={handleFileInput} accept="image/*" class="w-full p-3 border border-input-border-color rounded-xl" />
</div>
{/* <div>
<label for="confirm-password" class="block text-sm font-medium text-default"> <i class="bi bi-lock text-input-icon-color"></i></label>
<input id="confirm-password" autocomplete="new-password" onInput$={(e) => userState.password = (e.target as HTMLInputElement).value} type="password" class="w-full p-3 border border-input-border-color rounded-xl" placeholder="비밀번호 다시 입력" />
</div> */}
<button type="submit" class="w-full bg-button-color-1 h-12 text-default py-3 px-4 rounded-xl hover:bg-button-color-1/1 transition-colors">
</button>
{userState.error && <p class="text-center w-full text-red-500 ">{userState.error}</p>}
</form>
<div class="mt-4 text-center flex flex-row justify-center gap-2">
<p> ?</p>
<button onClick$={() => {isRegistering.value = false; userState.username = ''; userState.password = ''; userState.email = ''; userState.profile_file = null}} class="text-gray-500 hover:text-gray-500/80 text-sm font-medium"></button>
</div>
</div>
</div>
) : (
// 로그인
<div class="w-auto flex flex-col justify-center items-center">
<h1 class="text-5xl font-bold mb-2 text-center inline-block">Hello, CyWorld!</h1>
<p class="mb-2 text-xs font-semibold text-gray-custom opacity-70 text-center inline-block"> , . <i class="bi bi-emoji-smile"></i></p>
<div class="form-side w-full inline-block">
<form class="space-y-4" onSubmit$={handleSubmit} preventdefault:submit>
<div>
<label for="username" class="block text-sm font-medium text-default text-left"> <i class="bi bi-person text-input-icon-color"></i></label>
<input id="username" onInput$={(e) => userState.username = (e.target as HTMLInputElement).value} type="text" class="w-full p-3 border border-input-border-color rounded-xl" placeholder="이름 입력" />
</div>
<div>
<label for="password" class="block text-sm font-medium text-default text-left"> <i class="bi bi-lock text-input-icon-color"></i></label>
<input id="password" autocomplete="new-password" onInput$={(e) => userState.password = (e.target as HTMLInputElement).value} type="password" class="w-full p-3 border border-input-border-color rounded-xl" placeholder="비밀번호 입력" />
</div>
<button type="submit" class="w-full bg-button-color-1 h-12 text-default py-3 px-4 rounded-xl hover:bg-opacity-80 transition-colors">
</button>
{userState.error && <p class="text-center w-full text-red-500">{userState.error}</p>}
</form>
<div class="mt-4 text-center flex flex-row justify-center gap-2">
<p class="text-default"> ?</p>
<button onClick$={() => {isRegistering.value = true; userState.username = ''; userState.password = ''; userState.email = ''; userState.profile_file = null}} class="text-gray-500 hover:text-gray-500/80 text-sm font-medium"></button>
</div>
</div>
</div>
)}
</div>
</div>
);
});
export const head: DocumentHead = {
title: "Login / Sign Up",
meta: [
{
name: "description",
content: "Login or create a new account",
},
],
};

View file

@ -0,0 +1,42 @@
import { component$, Slot } from "@builder.io/qwik";
import type { DocumentHead, RequestHandler } from "@builder.io/qwik-city";
import axios from "axios";
export const onGet: RequestHandler = async ({ cookie, redirect }) => {
const jwt = cookie.get('access_token')?.value;
console.log(jwt + "이것은 auth로 들어온 token");
if (jwt) {
try {
const res = await axios.get('http://localhost:8000/api/user/profile/쌀숭이', {
headers: {
Authorization: `Bearer ${jwt}`,
},
});
console.log(res.data);
console.log(encodeURIComponent(res.data.username));
throw redirect(302, `/${encodeURIComponent(res.data.username)}/`);
} catch (error : any) {
console.error(error);
}
}
};
export default component$(() => {
return (
<>
<main class="bg-custom-gradient w-full h-full">
<Slot />
</main>
</>
);
});
export const head: DocumentHead = {
title: "Auth",
meta: [
{
name: "description",
content: "Auth page description",
},
],
};

14
src/routes/404.tsx Normal file
View file

@ -0,0 +1,14 @@
import { component$, Slot } from "@builder.io/qwik";
export default component$(() => {
return (
<div class="w-full h-full">
404 Not found
<div class="flex justify-center items-center h-full">
<button class="bg-button-color-1 h-12 text-default py-3 px-4 rounded-xl hover:bg-opacity-80 transition-colors">Home</button>
</div>
</div>
);
});

View file

@ -0,0 +1,63 @@
import { Link, RequestHandler } from "@builder.io/qwik-city";
// import { onRequest } from "../layout";
import { component$, Slot } from "@builder.io/qwik";
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}) => {
console.log(params.username + "의 상점 페이지로 들어옴");
try {
const res = await axios.get(`http://localhost:8000/api/store`, {
headers: {
Authorization: `Bearer ${cookie.get("access_token")?.value}`,
},
});
sharedMap.set("dotory", res.data ?? 0);
console.log(res.data ?? "no data");
} catch (error) {
console.log(error);
sharedMap.set("dotory", 0);
}
}
export const useDotory = routeLoader$(({sharedMap}) => {
return sharedMap.get("dotory") as number;
});
export default component$(() => {
const location = useLocation();
const dotory = useDotory();
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">
<div class="flex items-center justify-between w-full bg-diary-color rounded-lg p-2">
<div class="flex md:flex-row flex-col gap-2">
<Link href={`/${location.params.username}/store`} class={`${location.url.pathname === `/${location.params.username}/store` ? "bg-[#FAD659]" : "bg-white"} text-gray-800 px-4 py-2 rounded-lg font-medium`}> <i class="bi bi-shop"></i></Link>
<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>
</div>
<Slot />
</div>
);
});
export const head: DocumentHead = {
title: "Store",
meta: [
{
name: "description",
content: "Store page description",
},
],
};

View file

@ -0,0 +1,393 @@
import { $, component$, useSignal, useTask$, useVisibleTask$ } from "@builder.io/qwik";
import { DocumentHead } from "@builder.io/qwik-city";
import RoomGrid from "~/components/room/RoomGrid";
import { typechecker } from "~/utils/func";
import { routeLoader$ } from "@builder.io/qwik-city";
import axios from "axios";
import { getCookie } from "~/utils/cookie";
interface Furniture {
furniture_name: string;
image_path: string;
x : number;
y : number;
}
interface Avatar {
id: number;
user_id: number;
avatar_type: {
name: string;
path: string
},
top_clothe_type: {
name: string;
path: string
},
bottom_clothe_type: {
name: string;
path: string
}
}
interface SetAvatarType {
avatar_type: string;
top_clothe_type: string;
bottom_clothe_type: string;
}
interface RoomType {
type: string;
image_path: string;
}
interface Room {
id: number;
user_id: number;
room_type: string;
room_name: string;
room_image_path: string;
}
export const useRoomLoader = routeLoader$(async ({cookie}) => {
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}};
} catch (error : any) {
console.error(error);
return error.response?.data?.detail || 'Room not found';
}
})
export default component$(() => {
const data = useRoomLoader();
const selectedFurniture = useSignal<Furniture[]>([]); // 유저 선택 가구
const selectedType = useSignal("furniture"); // 헤더에서 가구, 아바타, 배경 구분
const selectedRoomType = useSignal("room_1");
const selectedAvatar = useSignal<SetAvatarType>({
avatar_type: "",
top_clothe_type: "",
bottom_clothe_type: ""
});
useVisibleTask$(() => {
selectedRoomType.value = data.value.myData.room.room.room_type;
selectedAvatar.value.avatar_type = data.value.myData.avatar.avatar_type.name;
selectedAvatar.value.top_clothe_type = data.value.myData.avatar.top_clothe_type.name;
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");
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);
}
})
const handleCancel = $(() => {
console.log("handleCancel");
})
return (
<div class="flex flex-col h-full p-4">
<h1 class="text-2xl font-bold text-[#4b3c28] mb-6"> </h1>
<div class="flex flex-col md:flex-row gap-6 flex-1">
{/* 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} />
</div>
</div>
{/* Controls */}
<div class="w-full md:w-80 flex flex-col gap-4">
{/* Category Tabs */}
<div class="flex gap-1 bg-gray-100 p-1 rounded-lg">
<button
onClick$={() => { selectedType.value = "furniture" }}
class={`flex-1 py-2 px-3 text-sm rounded-md font-medium transition-colors ${
selectedType.value === "furniture"
? "bg-white text-[#4b3c28] shadow-sm"
: "text-gray-600 hover:bg-gray-50"
}`}
>
</button>
<button
onClick$={() => { selectedType.value = "avatar" }}
class={`flex-1 py-2 px-3 text-sm rounded-md font-medium transition-colors ${
selectedType.value === "avatar"
? "bg-white text-[#4b3c28] shadow-sm"
: "text-gray-600 hover:bg-gray-50"
}`}
>
</button>
<button
onClick$={() => { selectedType.value = "background" }}
class={`flex-1 py-2 px-3 text-sm rounded-md font-medium transition-colors ${
selectedType.value === "background"
? "bg-white text-[#4b3c28] shadow-sm"
: "text-gray-600 hover:bg-gray-50"
}`}
>
</button>
</div>
{/* Items Grid */}
<div class="flex flex-col h-[600px] bg-white border-2 border-[#FAD659] rounded-lg overflow-hidden">
<h3 class="text-lg font-semibold p-4 text-[#4b3c28] border-b border-gray-200">
{selectedType.value === "furniture" ? "가구" :
selectedType.value === "avatar" ? "아바타" : "배경"}
</h3>
<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>
)))
:
selectedType.value === "avatar" ? (
<div class="flex flex-col gap-3 ">
<div>
<h4 class="font-medium text-gray-700 mb-2"> </h4>
<div class="grid grid-cols-3 gap-3">
{data.value.selectionData.avatarType.avatar_types.map((type: string, index: number) => (
<div
key={`type-${index}`}
class={`flex flex-col items-center p-2 rounded-lg hover:bg-gray-100 cursor-pointer ${selectedAvatar.value.avatar_type === type ? "border-[2px] border-[#FAD659]" : "border-2 border-gray-200"}`}
onClick$={() => {
selectedAvatar.value = {
...selectedAvatar.value,
avatar_type: type
};
console.log(selectedAvatar.value.avatar_type)}}
>
<div class="w-16 h-16 bg-gray-100 rounded-full flex items-center justify-center mb-1 overflow-hidden border-2 border-gray-200 hover:border-[#FAD659] transition-colors">
<img src={`http://localhost:8000/${typechecker(type)}`} alt="avatar" style={{imageRendering: "pixelated"}} class="object-contain w-16 h-16" />
</div>
<span class="text-xs text-gray-700">{type}</span>
</div>
))}
</div>
</div>
<div>
<h4 class="font-medium text-gray-700 mb-2"></h4>
<div class="grid grid-cols-3 gap-3">
{data.value.selectionData.avatarType.top_clothe_types.map((clothe: string, index: number) => (
<div
key={`top-${index}`}
class={`flex flex-col items-center p-2 rounded-lg hover:bg-gray-100 cursor-pointer ${selectedAvatar.value.top_clothe_type === clothe ? "border-[2px] border-[#FAD659]" : "border-2 border-gray-200"}`}
onClick$={() => {
selectedAvatar.value = {
...selectedAvatar.value,
top_clothe_type: clothe
};
console.log(selectedAvatar.value.top_clothe_type)}}
>
<div class="w-20 h-20 bg-gray-50 rounded-lg flex items-center justify-center mb-2 overflow-hidden border-2 border-gray-200 hover:border-[#FAD659] transition-colors">
<img src={`http://localhost:8000/${typechecker(clothe)}`} alt="avatar" style={{imageRendering: "pixelated"}} class="object-contain w-16 h-16" />
</div>
<span class="text-xs text-gray-700 text-center">{clothe}</span>
</div>
))}
</div>
</div>
<div>
<h4 class="font-medium text-gray-700 mb-2"></h4>
<div class="grid grid-cols-3 gap-3">
{data.value.selectionData.avatarType.bottom_clothe_types.map((bottom: string, index: number) => (
<div
key={`bottom-${index}`}
class={`flex flex-col items-center p-2 rounded-lg hover:bg-gray-100 cursor-pointer ${selectedAvatar.value.bottom_clothe_type === bottom ? "border-[2px] border-[#FAD659]" : "border-2 border-gray-200"}`}
onClick$={() => {
selectedAvatar.value = {
...selectedAvatar.value,
bottom_clothe_type: bottom
};
console.log(selectedAvatar.value.bottom_clothe_type)}}
>
<div class="w-20 h-20 bg-gray-50 rounded-lg flex items-center justify-center mb-2 overflow-hidden border-2 border-gray-200 hover:border-[#FAD659] transition-colors">
<img src={`http://localhost:8000/${typechecker(bottom)}`} alt="avatar" style={{imageRendering: "pixelated"}} class="object-contain w-full h-full" />
</div>
<span class="text-xs text-gray-700 text-center">{bottom}</span>
</div>
))}
</div>
</div>
</div>
) : (
<div class="space-y-4 w-full">
<h4 class="font-medium text-gray-700 mb-2"> </h4>
<div class="grid grid-cols-3 gap-4">
{data.value.selectionData.roomType.map((roomType: RoomType, index: number) => (
<div
key={`room-${index}`}
class="flex flex-col w-full items-center p-3 rounded-lg hover:bg-gray-50 cursor-pointer border-2 border-gray-200 hover:border-[#FAD659] transition-colors bg-white shadow-sm hover:shadow-md"
onClick$={() => {
console.log('Selected room type:', roomType);
selectedRoomType.value = roomType.type;
}}
>
<img
src={`http://localhost:8000/${roomType.image_path}`}
alt={roomType.type}
class="w-full h-20 object-cover rounded mb-2"
/>
<span class="text-sm font-medium text-gray-700">
{roomType.type}
</span>
</div>
))}
</div>
</div>
)
}
</div>
</div>
{/* Action Buttons */}
<div class="flex gap-2">
<button
class="flex-1 bg-[#FAD659] hover:bg-[#f8ce3a] text-[#4b3c28] font-bold py-2 px-4 rounded-lg transition-colors"
onClick$={() => {handleChange() }}
>
</button>
{/* <button
class="flex-1 bg-gray-200 hover:bg-gray-300 text-gray-700 font-bold py-2 px-4 rounded-lg transition-colors"
onClick$={() => {handleCancel()}}
>
</button> */}
</div>
</div>
</div>
</div>
</div>
);
});
export const head: DocumentHead = {
title: "Room",
meta: [
{
name: "description",
content: "Room page description",
},
],
};

View file

@ -0,0 +1,120 @@
import { component$, useSignal } from "@builder.io/qwik";
import { DocumentHead, routeLoader$ } from "@builder.io/qwik-city";
import axios from "axios";
import { BuyModal } from "~/components/features/shop/BuyModal";
import RoomGrid from "~/components/room/RoomGrid";
interface ShopItem {
name: string;
image_path: string;
width: number;
// Add other properties that your items might have
}
interface Avatar {
id: number;
user_id: number;
avatar_type: {
name: string;
path: string
},
top_clothe_type: {
name: string,
path: string
},
bottom_clothe_type: {
name: string,
path: string
}
}
interface ShopResponse {
storeData: ShopItem[];
avatarData: Avatar[]; // You might want to define a more specific type for avatar data
dotory: number;
}
export const useShopLoader = routeLoader$(async ({cookie}) => {
try {
const res = await axios.get(`http://localhost:8000/api/room/catalog`, {
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 dotory = await axios.get(`http://localhost:8000/api/store`, {
// headers: {
// Authorization: `Bearer ${cookie.get("access_token")?.value}`,
// },
// });
console.log(res.data);
console.log(avatar.data);
return {storeData: res.data, avatarData: avatar.data, dotory: 0};
} catch (error : any) {
console.error(error);
return { storeData: [], avatarData: null, dotory: 0};
}
});
export default component$(() => {
const data = useShopLoader() as { value: ShopResponse };
const selectedItem = useSignal<ShopItem | null>(null);
const showBuyModal = useSignal(false);
return (
<div class="relative flex p-4 justify-between items-center w-full h-full">
{/* 좌측 */}
<div class="grid grid-cols-2 overflow-auto h-full max-h-[calc(100vh-100px)] gap-2">
{data.value.storeData.slice(0, data.value.storeData.length / 2).map((item: ShopItem) => (
<div key={item.name} class={`flex items-center flex-col gap-2 rounded-t-lg` } onClick$={() => {if(selectedItem.value === item) selectedItem.value = null; else selectedItem.value = item;}}>
<img src={`http://localhost:8000/${item.image_path}`} alt={item.name} style={{imageRendering: "pixelated"}} class={` object-contain w-24 h-24 ${selectedItem.value === item ? "border-[2px] border-[#FAD659]" : ""}`} />
<span class="text-center font-bold rounded-b-lg w-24 h-12 bg-[#E5E2B6]">{item.name}</span>
</div>
))}
</div>
{/* 중앙 빈 박스 */}
<div class="w-64 bg-white border-text-default border-[2px] rounded-lg h-80 flex items-center justify-center">
{selectedItem.value &&
<div class="flex flex-col gap-4 items-center">
<span class="font-bold">{selectedItem.value.name}</span>
<span>: {selectedItem.value.width}</span>
<img src={`http://localhost:8000/${selectedItem.value?.image_path}`} alt="avatar" style={{imageRendering: "pixelated"}} class="object-contain w-32 h-32" />
<button
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>
</button>
</div>
}
{!selectedItem.value && <p> .</p>}
</div>
{/* 우측 */}
<div class="grid grid-cols-2 overflow-auto h-full max-h-[calc(100vh-100px)] gap-2">
{data.value.storeData.slice(data.value.storeData.length / 2, data.value.storeData.length).map((item: ShopItem) => (
<div key={item.name} class={`flex items-center flex-col gap-2 rounded-t-lg` } onClick$={() => {if(selectedItem.value === item) selectedItem.value = null; else selectedItem.value = item;}}>
<img src={`http://localhost:8000/${item.image_path}`} alt={item.name} style={{imageRendering: "pixelated"}} class={` object-contain w-24 h-24 ${selectedItem.value === item ? "border-[2px] border-[#FAD659]" : ""}`} />
<span class="text-center font-bold rounded-b-lg w-24 h-12 bg-[#E5E2B6]">{item.name}</span>
</div>
))}
</div>
{showBuyModal.value &&
<div class="bg-opacity-45 z-[1000] fixed inset-0 w-full h-full bg-black"
onclick$={() => {showBuyModal.value = false}}>
<BuyModal showBuyModal={showBuyModal} item={selectedItem.value} mydotory={data.value.dotory} itemdotory={80}/>
</div>
}
</div>
);
});
export const head: DocumentHead = {
title: "Shop",
meta: [
{
name: "description",
content: "Shop page description",
},
],
};

View file

@ -0,0 +1,44 @@
import { component$, useSignal } from "@builder.io/qwik";
import { DocumentHead , RequestHandler, routeLoader$} from "@builder.io/qwik-city";
import { 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';
}
});
export default component$(() => {
const navigate = useNavigate();
const diaryData = diaryLoader();
return (
<div>
<h1> </h1>
</div>
);
});
export const head: DocumentHead = {
title: "일기수정하기",
meta: [
{
name: "description",
content: "유저가 쓴 일기의 상세정보",
},
],
};

View file

@ -0,0 +1,103 @@
import { component$, $, useSignal, useContext } from "@builder.io/qwik";
import { DocumentHead, useNavigate, routeLoader$, Link, useLocation } from "@builder.io/qwik-city";
import axios from "axios";
import { getCookie } from "~/utils/cookie";
// import { AppStateContext } from "~/utils/context";
type DiaryResponse = {diary: DiaryEntry, myInfo: number } | { detail: string };
export const useDiaryLoader = routeLoader$<DiaryResponse>(async ({params, cookie, status}) => {
const diary_id = params.diary_id;
try {
console.log(diary_id);
const res = await axios.get(`http://localhost:8000/api/diary/${diary_id }`);
const myInfo = await axios.get(`http://localhost:8000/api/user/me`, {
headers: {
Authorization: `Bearer ${cookie.get("access_token")?.value}`,
},
});
console.log(myInfo.data.id);
console.log(res.data);
return {diary: res.data, myInfo: myInfo.data.id};
} catch (error : any) {
console.error(error);
return error.response?.data?.detail || 'Diary not found';
}
});
export default component$(() => {
const navigate = useNavigate();
const location = useLocation();
const diaryLoader = useDiaryLoader();
// const AppState = useContext(AppStateContext);
const handleDelete = $(() => {
if ('diary' in diaryLoader.value && 'id' in diaryLoader.value.diary) {
try {
axios.delete(`http://localhost:8000/api/diary/${diaryLoader.value.diary.id}`,
{
headers: {
Authorization: `Bearer ${getCookie("access_token")}`,
},
}
);
} catch (error : any) {
console.error(error);
}
}
navigate(`/${location.params.username}/diary`);
});
if ('detail' in diaryLoader.value) {
return <div class="p-6">Diary not found or error: {diaryLoader.value.detail}</div>;
}
const { diary, myInfo } = diaryLoader.value;
return (
<div class="p-6">
<div class="flex border-b-2 border-text-default justify-between items-center mb-8">
<h1 class="text-2xl font-bold">{diary.title}</h1>
<div class="flex flex-row justify-between items-center">
{diary.user_id === myInfo && (
<div class="flex gap-3">
<button onClick$={handleDelete} >
<i class="bi bi-trash-fill text-3xl text-black"></i>
</button>
<Link href={`/${location.params.username}/diary/${location.params.diary_id}/edit/`}>
<i class="bi bi-send-fill text-3xl text-black"></i>
</Link>
</div>
)}
</div>
</div>
<div class="mt-8 flex flex-col gap-4">
<div class="flex gap-4 overflow-auto">
{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" />
))}
</div>
<p class="whitespace-pre-wrap ">{diaryLoader.value.diary.content}</p>
</div>
{('detail' in diaryLoader.value) && (
<div class="flex border-b-2 border-text-default justify-center items-center w-full h-full">
<p> .</p>
</div>
)}
</div>
);
});
export const head: DocumentHead = {
title: "일기",
meta: [
{
name: "description",
content: "유저가 쓴 일기의 상세정보",
},
],
};

View file

@ -0,0 +1,135 @@
import { component$, useSignal, $, useTask$, 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";
import { AppStateContext } from "~/utils/context";
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`, {
headers: {
Authorization: `Bearer ${cookie.get("access_token")?.value}`,
},
});
console.log(res.data);
return {user:userid.data, diaries: res.data};
} catch (error : any) {
console.error(error);
return error.response?.data?.detail || 'Diary not found';
}
});
export default component$(() => {
const navigate = useNavigate();
const location = useLocation();
const entryLoader = useDiaryLoader();
const entries = useSignal<DiaryEntry[]>([]);
const filteredEntries = useSignal<DiaryEntry[]>([]);
useTask$(() => {
console.log( entryLoader.value.user);
entries.value = entryLoader.value.diaries as DiaryEntry[];
filteredEntries.value = entries.value;
});
const userData = useContext(AppStateContext);
const searchQuery = useSignal('');
const selectedMonth = useSignal(new Date().getMonth());
const showMonth = useSignal(false);
const handleMonthChange = $(() => {
showMonth.value = !showMonth.value;
});
const handleMonthSelect = $((month : number) => {
selectedMonth.value = month;
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;
}
else {
filteredEntries.value = entries.value.filter((entry) => entry.title.includes(searchQuery.value));
}
console.log(filteredEntries.value);
});
return (
<div class="w-full h-full mx-auto p-8">
<div class="flex justify-between items-center md:flex-row flex-col mb-6 relative">
<div class="relative">
<button onClick$={() => handleMonthChange()} 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">
{selectedMonth.value} <i class={`bi ${showMonth.value ? 'bi-caret-up' : 'bi-caret-down'} transition-transform duration-200`}></i>
</button>
<div class={`absolute top-12 left-0 w-48 bg-white rounded-lg shadow-lg overflow-hidden transition-all duration-300 transform origin-top ${showMonth.value ? 'opacity-100 scale-y-100' : 'opacity-0 scale-y-95 pointer-events-none'}`}>
<ul class="max-h-60 overflow-y-auto scrollbar-thin">
{Array.from({ length: 12 }, (_, i) => i + 1).map((month) => (
<li key={month} onClick$={() => { handleMonthSelect(month); }} class={`px-4 py-2 hover:bg-gray-100 cursor-pointer transition-colors ${ month === selectedMonth.value ? 'bg-diary-color/30 font-bold' : '' }`}>
{month}
</li>
))}
</ul>
</div>
</div>
<div class="flex lg:flex-row flex-col gap-2 align-center justify-center">
{/* <i class="bi bi-sort-down h-4 w-4 text-center"></i> */}
<div class=" bg-diary-icon-hover rounded-3xl">
<form class="flex items-center gap-2" preventdefault:submit onSubmit$={handleSearch}>
<input type="text" id="search" class="px-4 py-2 rounded-3xl bg-diary-color/30 focus:outline-none" bind:value={searchQuery}/>
<button type="submit" class="text-black px-4 py-2 rounded-3xl font-bold transition-colors flex items-center gap-2">
<i class="bi bi-search"></i>
</button>
</form>
</div>
{entryLoader.value.user.username === userData.username && (
<button onClick$={() => navigate(`/${location.params.username}/diary/write`)} class="bg-diary-color hover:bg-diary-icon-hover text-black px-4 py-2 rounded-3xl font-bold transition-colors flex items-center gap-2">
<i class="bi bi-pencil-fill"></i>
</button>
)}
</div>
</div>
<div class="bg-white rounded-lg border-t-2 border-b-2 border-text-default overflow-hidden">
<div class="divide-y divide-text-default">
<div class="px-6 py-2 bg-diary-color transition-colors cursor-pointer">
<div class="flex items-center justify-between">
<span class="text-base flex-1 text-center whitespace-nowrap overflow-hidden text-ellipsis text-black font-bold text-default"></span>
<span class="text-sm flex-1 text-end whitespace-nowrap overflow-hidden text-ellipsis text-black font-bold text-default"></span>
</div>
</div>
{ filteredEntries.value.map((entry, index) => (
<div key={index} class="px-6 py-2 hover:bg-gray-50 transition-colors cursor-pointer">
<Link href={`/${location.params.username}/diary/${entry.id}`} class="flex items-center justify-between">
<span class="text-base flex-1 text-center whitespace-nowrap overflow-hidden text-ellipsis font-medium text-default">{entry.title}</span>
<span class="text-sm flex-1 text-end whitespace-nowrap overflow-hidden text-ellipsis font-medium text-default">{new Date(entry.created_at).toLocaleString()}</span>
</Link>
</div>
))}
{Array.from({length: 15 - filteredEntries.value.length}, (_, index) => (
<div key={index+filteredEntries.value.length} class="px-6 py-2 hover:bg-gray-50 transition-colors cursor-pointer">
{/* <Link href={`/${location.params.username}/diary/`} class="flex items-center justify-between"> */}
<span class="text-base flex-1 text-center whitespace-nowrap overflow-hidden text-ellipsis font-medium text-default"> &nbsp;</span>
<span class="text-sm flex-1 text-end whitespace-nowrap overflow-hidden text-ellipsis font-medium text-default">&nbsp;</span>
{/* </Link> */}
</div>
))}
</div>
</div>
</div>
);
});
export const head: DocumentHead = {
title: "나의 일기장",
meta: [
{
name: "description",
content: "나만의 일기장. 매일의 추억을 기록해보세요.",
},
],
};

View file

@ -0,0 +1,250 @@
import { $, component$, useSignal, useVisibleTask$, NoSerialize } from "@builder.io/qwik";
import { useLocation, useNavigate } from "@builder.io/qwik-city";
import axios from "axios";
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;
}
export default component$(() => {
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>();
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;
}
});
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.post('http://localhost:8000/api/diary', 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

@ -0,0 +1,211 @@
import { component$, useSignal, $, useTask$} 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";
import axios from "axios";
import DeleteModal from "~/components/features/guestbook/DeleteModal";
interface GuestbookContent{
id: number,
content: string,
target_user_id: number,
user_id: number,
user_profile_path: string,
username: string,
created_at: string
}
interface GuestbookResponse {
messages: GuestbookContent[];
myInfo: {
id: number,
username: string,
email: string,
created_at: string,
profile_image_path: string,
is_active: boolean
};
targetUser: {
id: number,
username: string,
email: string,
created_at: string,
profile_image_path: string,
is_active: boolean
};
}
export const useGuestbookLoader = routeLoader$(async ({params, cookie}) => {
try {
console.log(params.username + "의 방명록 가져오기");
const userid = await axios.get(`http://localhost:8000/api/user/profile/${params.username}`);
const myid = await axios.get(`http://localhost:8000/api/user/me`, {
headers: {
Authorization: `Bearer ${cookie.get("access_token")?.value}`,
},
});
const res = await axios.get<GuestbookContent[]>(`http://localhost:8000/api/guestbook/${userid.data.id}`);
console.log(res.data);
return {
messages: res.data,
myInfo: myid.data,
targetUser: userid.data,
} as GuestbookResponse;
} catch (error) {
console.error(error);
return {
messages: [],
myInfo: { id: 0, username: 'Guest' ,email: 'Guest', created_at: 'Guest', profile_image_path: 'Guest', is_active: false},
targetUser: { id: 0, username: 'Guest' ,email: 'Guest', created_at: 'Guest', profile_image_path: 'Guest', is_active: false}
} as GuestbookResponse;
}
});
export default component$(() => {
const location = useLocation();
const username = location.params.username;
const loaderData = useGuestbookLoader();
const messages = useSignal<GuestbookContent[]>([]);
const newMessage = useSignal('');
const showUpdateModal = useSignal(false);
const showDeleteModal = useSignal(false);
const selectedMessage = useSignal<GuestbookContent>({} as GuestbookContent);
const updatedMessage = useSignal('');
const refreshMessages = $(async () => {
const res = await axios.get<GuestbookContent[]>(`http://localhost:8000/api/guestbook/${loaderData.value.targetUser.id}`);
messages.value = res.data;
});
useTask$(() => {
console.log("useTask");
refreshMessages();
messages.value = loaderData.value.messages;
});
const handleSubmit = $(() => {
if (!newMessage.value.trim()) return;
try {
axios.post(`http://localhost:8000/api/guestbook/`, {
content: newMessage.value,
target_user_id: loaderData.value.targetUser.id,
}, {
headers: {
Authorization: `Bearer ${getCookie("access_token")}`,
},
});
console.log("방명록 추가");
refreshMessages();
} catch (error) {
console.error(error);
}
newMessage.value = '';
});
const handleEdit = $(() => {
try {
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);
}
showUpdateModal.value = false;
updatedMessage.value = '';
});
return (
<div class="flex flex-col h-screen bg-gray-50">
{showDeleteModal.value && <DeleteModal messages={messages} showDeleteModal={showDeleteModal} selectedMessage={selectedMessage}/>}
<div class="flex-1 overflow-y-auto p-4 space-y-4 divide-y-2 divide-gray-200">
{messages.value.map((msg) => (
<div key={msg.id} class="flex relative items-start pt-2 space-x-3">
<div class={`w-14 h-14 rounded-full text-black`}>
<img class="w-full h-full object-cover rounded-full" src={`http://localhost:8000/${msg.user_profile_path}`} alt="" />
</div>
<div class="flex-1">
<div class="text-sm text-gray-500">
<span class=" flex items-center gap-2"><span class="text-xl font-bold text-black">{msg.username}</span> <span class="text-xs font-medium text-gray-400">{new Date(msg.created_at).toLocaleString("ko-KR", { timeZone: "Asia/Seoul", year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", second: "2-digit", hour12: false, })}</span></span>
</div>
<div class="whitespace-pre-wrap text-gray-800">
{msg.content}
</div>
</div>
{msg.user_id === loaderData.value.myInfo.id &&
<div class="flex space-x-2">
<button
onClick$={() => {
selectedMessage.value = msg;
updatedMessage.value = msg.content;
showUpdateModal.value = true;
}}
class="text-gray-400 hover:text-blue-500 px-2 py-1 text-sm"
>
<i class="bi bi-pencil-fill"></i>
</button>
<button
onClick$={() => {
selectedMessage.value = msg;
showDeleteModal.value = true;
}}
class="text-gray-400 hover:text-red-500 px-2 py-1 text-sm"
>
<i class="bi bi-trash-fill"></i>
</button>
</div>
}
{showUpdateModal.value && msg.id === selectedMessage.value.id && (
<div
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()} /* 외부 클릭으로 닫히는 건 부모가 처리하게 둘 수 있음 */
>
<div class="flex mt-2 justify-between">
<p> .</p>
<div class="flex gap-2 mt-2 justify-end">
<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}>
<i class="bi bi-check"></i>
</button>
</div>
</div>
<input
value={updatedMessage.value}
onInput$={(ev: any) => (updatedMessage.value = ev.target.value)}
class="w-full p-2 border border-input-border-color rounded-xl"
/>
</div>
)}
</div>
))}
</div>
<div class="p-4 bg-white">
<form preventdefault:submit onSubmit$={handleSubmit} class=" relative flex">
<input
bind:value={newMessage}
type="text"
placeholder="방명록을 입력해 주세요."
class="flex-1 p-2 rounded-lg border bg-input-color text-white placeholder:text-white border-gray-300 focus:outline-none focus:ring-1 focus:ring-[#4b3c28]"
/>
{/* <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">
<i class="bi bi-send text-white text-center"></i>
</button>
</form>
</div>
</div>
);
});
export const head: DocumentHead = {
title: "방명록",
meta: [
{
name: "description",
content: "방명록 페이지입니다. 자유롭게 메시지를 남겨보세요!",
},
],
};

View file

@ -0,0 +1,161 @@
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 { AppStateContext } from "~/utils/context";
import { getCookie } from "~/utils/cookie";
interface FriendRequest {
id: number;
user_id: number;
friend_id: number;
friend_username: string;
status: string;
created_at: string;
}
export const useDataLoader = routeLoader$(async ({params, cookie}) => {
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}`,
},
});
// 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} };
} catch (error : any) {
console.error(error);
return {diaries: [], friendRequest: [] as FriendRequest[], userdata: {room: [], avatar: [] } };
}
});
export default component$(() => {
const data = useDataLoader();
const showMyFriendsRequest = useSignal<boolean>(false);
const userData = useContext(AppStateContext);
const selectedRoom = useSignal({});
const location = useLocation();
useVisibleTask$(() => {
selectedRoom.value = data.value.userdata.room;
// userData.avatar = data.value.userdata.avatar;
})
const handleAcceptFriend = $( async (friendship_id : number) => {
try {
console.log(`http://localhost:8000/api/friendship/${friendship_id}/accept`);
const res = await axios.put(`http://localhost:8000/api/friendship/${friendship_id}/accept`, {
headers: {
Authorization: `Bearer ${getCookie("access_token")}`,
},
})
data.value.friendRequest = data.value.friendRequest.filter((friend) => friend.id !== friendship_id);
} catch (error : any) {
console.log(error.response?.data?.detail);
}
})
return (
<div class="min-h-screen bg-white px-8 py-4 text-[#4b3c28]">
<header class="flex justify-between items-start border-b border-[#4b3c28] pb-1 mb-4">
<h1 class="text-3xl font-bold">Title</h1>
<a href={`/${userData.username}`} class="text-sm underline"> </a>
</header>
<div class="flex flex-col lg:flex-row gap-8">
<div class="flex-1">
<section class="mb-6">
<div class="flex justify-between">
<h2 class="text-xl font-bold mb-2">Mini Room</h2>
<Link href="/miniroom" class="text-base text-[#4b3c28] font-semibold">
<i class="bi bi-caret-right"></i>
</Link>
</div>
<div>
<div class="flex flex-col lg:flex-row gap-8">
{/* <RoomGrid furniture={data.value.room} avatar={data.value.avatar} roomType={data.value.roomType} /> */}
<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>
<ul class="space-y-2">
{data.value.diaries.map((diary : any) => (
<li key={diary.id} class="flex items-center gap-2 md:text-sm">
<Link href={`/${location.params.username}/diary/${diary.id}`} class="flex items-center gap-2 md:text-sm">
<span class="bg-[#4b3c28] text-white text-xs w-12 px-2 rounded"></span>
<span class="text-xs lg:text-sm">{diary.title}</span>
</Link>
</li>
))}
</ul>
</div>
<div class="flex-1 pt-7 justify-start w-full lg:w-[220px] text-base lg:mt-0">
<div class="border-t border-dashed border-[#4b3c28] pt-2 pb-2">
<div class="flex justify-between">
<span class="flex-1"> {data.value.diaries.length}</span>
{userData.username === location.params.username && <span onclick$={() => {showMyFriendsRequest.value = true}} class="flex-1 hover:text-[#4b3c28] cursor-pointer hover:underline"> {data.value.friendRequest.length}</span>}
</div>
</div>
<div class="border-t border-b border-dashed border-[#4b3c28] pt-2 pb-2">
<span class="flex-1"> 4</span>
</div>
</div>
</section>
</div>
</div>
</section>
{showMyFriendsRequest.value && (
<div onclick$={() => {showMyFriendsRequest.value = false}} class="bg-opacity-45 z-[1000] fixed inset-0 w-full h-full py-6 px-6 flex justify-center items-center bg-black">
<div onclick$={(e) => {e.stopPropagation()}} class="flex w-full h-full flex-col max-w-[1000px] p-2 max-h-[400px] bg-white">
<div class="w-full flex justify-between"><p class="text-lg font-bold p-2 w-full text-left"> </p>
<i class="bi bi-x-lg cursor-pointer" onclick$={() => {showMyFriendsRequest.value = false}}></i></div>
{data.value.friendRequest.length > 0 ? (
<div class="divide-y-2 divide-gray-200 max-h-64 w-full overflow-y-auto">
{data.value.friendRequest.map((friend, i) => (
<div key={i} class="flex items-center justify-between border-b-2 border-gray-200 py-3">
<div class="flex items-center gap-3"><span class="font-medium text-sm">{friend.friend_username}</span></div>
<button class="text-black hover:text-red-500" onclick$={() => {handleAcceptFriend(friend.id)}}><i class="bi bi-check"></i></button>
</div>
))}
</div>
) : (
<div class="flex items-center justify-center h-full">
<p class="text-gray-500"> .</p>
</div>
)}
</div>
</div>
)}
</div>
</div>
</div>
);
});
export const head: DocumentHead = {
title: "내 미니홈피",
meta: [
{
name: "description",
content: "나만의 공간에 오신 것을 환영합니다!",
},
],
};

View file

@ -0,0 +1,72 @@
import { component$, Slot, useServerData, useSignal } from "@builder.io/qwik";
import { type DocumentHead, RequestHandler, routeLoader$ } from "@builder.io/qwik-city";
import Header from "~/components/layout/Header";
import { AddFriends } from "~/components/features/AddFriends";
import axios from "axios";
import jwt from "jsonwebtoken";
import { useContextProvider } from "@builder.io/qwik";
import { AppStateContext } from "~/utils/context";
import { deleteCookie } from "~/utils/cookie";
export const onRequest: RequestHandler = async ({ cookie, sharedMap, redirect , params}) => {
const token = cookie.get("access_token")?.value;
console.log(token + "이것은 유저페이지로 들어온 token");
if (!token) {
console.log("no token");
throw redirect(302, "/auth");
}
try {
// jwt.verify(token, process.env.JWT_SECRET!);
const userInfo = await axios.get(`http://localhost:8000/api/user/profile/${params.username}`, {
headers: {
Authorization: `Bearer ${token}`,
},
});
console.log("data : " + userInfo.data);
console.log("token verified");
const res = await axios.get('http://localhost:8000/api/user/me', {
headers: {
Authorization: `Bearer ${token}`,
},
});
sharedMap.set("user", res.data);
} catch (e : any) {
if(e.response.status === 404) {
console.log("user not found");
throw redirect(302, "/");
}
else {
console.log("token not verified");
cookie.delete("access_token");
throw redirect(302, "/auth");
}
}
};
export const useUser = routeLoader$(({sharedMap}) => {
return sharedMap.get("user");
});
export default component$(() => {
const addFriends = useSignal<boolean>(false);
const userData = useUser();
useContextProvider(AppStateContext, userData.value);
return (
<div class="flex min-h-screen">
<Header showAddFriends={addFriends} />
{addFriends.value == true ? <AddFriends showAddFriends={addFriends} /> : null}
<main class="flex-1 min-h-screen">
<Slot />
</main>
</div>
);
});
export const head: DocumentHead = {
title: "2025 AnA SSF Pages",
meta: [
{
name: "description",
content: "2025 AnA SSF Pages with Qwik",
},
],
};

View file

@ -1,25 +1,34 @@
import { routeLoader$ } from "@builder.io/qwik-city";
import { component$ } from "@builder.io/qwik";
import type { DocumentHead } from "@builder.io/qwik-city";
import axios from "axios";
export const useRedirectLoader = routeLoader$(async ({ cookie, redirect }) => {
const token = cookie.get("access_token")?.value;
console.log(token + "메인으로가면 생기는 토큰");
let res;
if (token) {
// 쿠키에서 username 추출 (JWT decode 하거나 DB 조회 가능)
try {
res = await axios.get('http://localhost:8000/api/user/me', {
headers: {
Authorization: `Bearer ${token}`,
},
});
console.log(res.data.username + "데이터 받아옴");
} catch (error : any) {
console.error(error + "에러");
cookie.delete("access_token");
throw redirect(302, "/auth");
}
} else {
console.log("no token");
throw redirect(302, "/auth");
}
if (res.data.username) {
throw redirect(302, `/${encodeURIComponent(res.data.username)}/`);
}
});
export default component$(() => {
return (
<>
<h1>Hi 👋</h1>
<div>
Can't wait to see what you build with qwik!
<br />
Happy coding.
</div>
</>
);
});
export const head: DocumentHead = {
title: "Welcome to Qwik",
meta: [
{
name: "description",
content: "Qwik site description",
},
],
};
export default component$(() => {
// 여긴 사실 실행 안 됨 (위에서 리다이렉트됨)
return <div>Loading...</div>;
});

8
src/tailwind.css Normal file
View file

@ -0,0 +1,8 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
html {
@apply text-[#644B23];
}
}

16
src/types/Friends.ts Normal file
View file

@ -0,0 +1,16 @@
interface Friend {
id: number,
username: string,
email: string,
created_at: string,
profile_image_path: string,
is_active: boolean
}
interface User {
id: number,
username: string,
email: string,
created_at: string,
profile_image_path: string,
is_active: boolean
}

11
src/types/diary.ts Normal file
View file

@ -0,0 +1,11 @@
interface DiaryEntry {
id: number,
user_id: number,
title: string,
content: string,
images: string[],
category: string,
created_at: string,
is_submitted: boolean,
email_sent: boolean
}

5
src/utils/context.ts Normal file
View file

@ -0,0 +1,5 @@
import { createContextId } from '@builder.io/qwik';
export const AppStateContext = createContextId<User>('auth-state');

14
src/utils/cookie.ts Normal file
View file

@ -0,0 +1,14 @@
export function setCookie(name: string, value: string, days = 7) {
const expires = new Date();
expires.setTime(expires.getTime() + days * 24 * 60 * 60 * 1000);
document.cookie = `${name}=${encodeURIComponent(value)};expires=${expires.toUTCString()};path=/`;
}
export function deleteCookie(name: string) {
document.cookie = `${name}=; expires=${new Date(0).toUTCString()}; path=/`;
}
export function getCookie(name: string) {
const value = `; ${document.cookie}`;
const parts = value.split(`; ${name}=`);
if (parts.length === 2) return parts.pop()?.split(';').shift() || null;
return null;
}

21
src/utils/func.ts Normal file
View file

@ -0,0 +1,21 @@
const avatar_path = {
: "public/avatar/남자.png",
: "public/avatar/여자.png",
: "public/avatar/교복상의.png",
: "public/avatar/교복조끼상의.png",
AnA동잠: "public/avatar/동잠상의.png",
: "public/avatar/멜빵바지상의.png",
: "public/avatar/멜빵치마상의.png",
: "public/avatar/무지개맨투맨상의.png",
: "public/avatar/산타상의.png",
: "public/avatar/교복하의남.png",
: "public/avatar/교복하의여.png",
: "public/avatar/교복조끼하의남.png",
: "public/avatar/교복조끼하의여.png",
: "public/avatar/산타하의.png",
: "public/avatar/청바지하의.png",
}
export const typechecker = (type: string) => {
// console.log(type.replace(/\s+/g, ""));
return avatar_path[type.replace(/\s+/g, "") as keyof typeof avatar_path] ?? undefined;
}

11
src/utils/store.ts Normal file
View file

@ -0,0 +1,11 @@
// import { useStore } from '@builder.io/qwik';
// export type AuthState = {
// userna: string | null;
// };
// // 초기값을 그대로 export하지 않고, 함수로 생성
// export const createAppState = () => useStore<AuthState>({
// user: null,
// });

29
tailwind.config.js Normal file
View file

@ -0,0 +1,29 @@
/** @type {import('tailwindcss').Config} */
export default {
content: [
"./src/**/*.{js,ts,jsx,tsx,mdx}"],
theme: {
extend: {
colors: {
'gradient-start': '#FEE182',
'gradient-end': '#FFC47E',
'text-default': '#644B23',
'text-gray-custom': '#B1A490',
'input-icon-color': '#FFAD84',
'button-color-1': '#FFAD84',
'button-color-2': '#FFAD84',
'input-border-color': '#FFC47E',
'diary-color': '#FFE382',
'diary-icon-hover' : '#FFE39D',
'button-color-3': '#FFC47E',
'input-color': '#FFC47E',
},
backgroundImage: {
'custom-gradient': 'linear-gradient(180deg, #FEE182 11%, #FFC47E 100%)',
'signup-icon' : 'linear-gradient(180deg, #FFF78A 0%, #FFAD84 100%)'
},
},
},
plugins: [],
}

View file

@ -7,6 +7,8 @@ import { qwikVite } from "@builder.io/qwik/optimizer";
import { qwikCity } from "@builder.io/qwik-city/vite";
import tsconfigPaths from "vite-tsconfig-paths";
import pkg from "./package.json";
import tailwindcss from "tailwindcss";
import autoprefixer from "autoprefixer";
type PkgDep = Record<string, string>;
const { dependencies = {}, devDependencies = {} } = pkg as any as {
@ -23,6 +25,11 @@ export default defineConfig(({ command, mode }): UserConfig => {
return {
plugins: [qwikCity(), qwikVite(), tsconfigPaths({ root: "." })],
// This tells Vite which dependencies to pre-build in dev mode.
css: {
postcss: {
plugins: [tailwindcss, autoprefixer],
},
},
optimizeDeps: {
// Put problematic deps that break bundling here, mostly those with binaries.
// For example ['better-sqlite3'] if you use that in server functions.