diff --git a/src/components/domains/user/components/ScrollRowSection.tsx b/src/components/domains/user/components/ScrollRowSection.tsx
index 8a621b8e..f2507301 100644
--- a/src/components/domains/user/components/ScrollRowSection.tsx
+++ b/src/components/domains/user/components/ScrollRowSection.tsx
@@ -1,5 +1,5 @@
import Image from "next/image";
-import ICONS from "@/lib/constants/icons";
+import ICONS from "@/shared/constants/icons";
export const ScrollRowSection = ({
title,
diff --git a/src/components/domains/user/sections/ActivitySection/components/LikePerfumeList.tsx b/src/components/domains/user/sections/ActivitySection/components/LikePerfumeList.tsx
index 2b0683be..cdc6b3c1 100644
--- a/src/components/domains/user/sections/ActivitySection/components/LikePerfumeList.tsx
+++ b/src/components/domains/user/sections/ActivitySection/components/LikePerfumeList.tsx
@@ -1,7 +1,7 @@
import React from "react";
import Link from "next/link";
import PerfumeCard from "@/components/commons/card/perfumeCard";
-import type { ApiPerfumeSimpleResponse } from "@/lib/hono/schemas/perfume.schema";
+import type { ApiPerfumeSimpleResponse } from "@/server/hono/schemas/perfume.schema";
export const LikePerfumeList = ({
likedPerfumes,
diff --git a/src/components/domains/user/sections/ActivitySection/components/LikePostList.tsx b/src/components/domains/user/sections/ActivitySection/components/LikePostList.tsx
index 99790201..e8209d75 100644
--- a/src/components/domains/user/sections/ActivitySection/components/LikePostList.tsx
+++ b/src/components/domains/user/sections/ActivitySection/components/LikePostList.tsx
@@ -1,8 +1,8 @@
import React from "react";
import Link from "next/link";
import { PostCard } from "@/components/commons/card/postCard";
-import { POST_CARD_TYPES } from "@/lib/constants/post";
-import type { ApiPostResponse } from "@/lib/hono/schemas/community.schema";
+import { POST_CARD_TYPES } from "@/shared/constants/post";
+import type { ApiPostResponse } from "@/server/hono/schemas/community.schema";
export const LikePostList = ({
likedPosts,
diff --git a/src/components/domains/user/sections/ActivitySection/components/MyCommentList.tsx b/src/components/domains/user/sections/ActivitySection/components/MyCommentList.tsx
index 220e9e97..d11c32ae 100644
--- a/src/components/domains/user/sections/ActivitySection/components/MyCommentList.tsx
+++ b/src/components/domains/user/sections/ActivitySection/components/MyCommentList.tsx
@@ -1,6 +1,6 @@
import React from "react";
import Reply from "@/components/domains/user/Reply";
-import { MyComment } from "@/lib/hono/services/me.service";
+import { MyComment } from "@/server/hono/services/me";
export const MyCommentsList = ({ comments }: { comments: MyComment[] }) => {
return comments.map((item, idx) => (
diff --git a/src/components/domains/user/sections/ActivitySection/components/MyPostList.tsx b/src/components/domains/user/sections/ActivitySection/components/MyPostList.tsx
index 8bab2cb9..42e2f324 100644
--- a/src/components/domains/user/sections/ActivitySection/components/MyPostList.tsx
+++ b/src/components/domains/user/sections/ActivitySection/components/MyPostList.tsx
@@ -1,8 +1,8 @@
import React from "react";
import Link from "next/link";
import { PostCard } from "@/components/commons/card/postCard";
-import { POST_CARD_TYPES } from "@/lib/constants/post";
-import type { ApiPostResponse } from "@/lib/hono/schemas/community.schema";
+import { POST_CARD_TYPES } from "@/shared/constants/post";
+import type { ApiPostResponse } from "@/server/hono/schemas/community.schema";
export const MyPostList = ({ posts }: { posts: ApiPostResponse[] }) => {
return posts.length > 0 ? (
diff --git a/src/components/domains/user/sections/ActivitySection/components/MyReviewList.tsx b/src/components/domains/user/sections/ActivitySection/components/MyReviewList.tsx
index 25fa9595..d3d4af29 100644
--- a/src/components/domains/user/sections/ActivitySection/components/MyReviewList.tsx
+++ b/src/components/domains/user/sections/ActivitySection/components/MyReviewList.tsx
@@ -1,6 +1,6 @@
import React from "react";
import ReviewCard from "@/components/commons/card/reviewCard";
-import type { ApiReviewResponse } from "@/lib/hono/schemas/review.schema";
+import type { ApiReviewResponse } from "@/server/hono/schemas/review.schema";
export const MyReviewList = ({ reviews }: { reviews: ApiReviewResponse[] }) => {
return reviews.length > 0 ? (
diff --git a/src/components/domains/user/sections/ActivitySection/index.tsx b/src/components/domains/user/sections/ActivitySection/index.tsx
index 0f807006..735641cc 100644
--- a/src/components/domains/user/sections/ActivitySection/index.tsx
+++ b/src/components/domains/user/sections/ActivitySection/index.tsx
@@ -10,7 +10,7 @@ import {
SkeletonComment,
SkeletonPerfume,
} from "../../components/skeletons";
-import { useMediaQuery } from "@/lib/hooks/useMediaQuery";
+import { useMediaQuery } from "@/client/hooks/useMediaQuery";
const MyReviewListLoader = dynamic(
() => import("./loader/MyReviewListLoader"),
diff --git a/src/components/domains/user/sections/ActivitySection/loader/LikePerfumeListLoader.tsx b/src/components/domains/user/sections/ActivitySection/loader/LikePerfumeListLoader.tsx
index c22453d4..ca882b4c 100644
--- a/src/components/domains/user/sections/ActivitySection/loader/LikePerfumeListLoader.tsx
+++ b/src/components/domains/user/sections/ActivitySection/loader/LikePerfumeListLoader.tsx
@@ -1,7 +1,7 @@
"use client";
import { LikePerfumeList } from "../components";
-import { useUserLikedPerfume } from "@/lib/hooks/query/useUserQuery";
+import { useUserLikedPerfume } from "@/client/hooks/query/useUserQuery";
export default function LikePerfumeListLoader() {
const { data: likedPerfumes, error } = useUserLikedPerfume();
diff --git a/src/components/domains/user/sections/ActivitySection/loader/LikePostListLoader.tsx b/src/components/domains/user/sections/ActivitySection/loader/LikePostListLoader.tsx
index 843b6b12..57665cab 100644
--- a/src/components/domains/user/sections/ActivitySection/loader/LikePostListLoader.tsx
+++ b/src/components/domains/user/sections/ActivitySection/loader/LikePostListLoader.tsx
@@ -1,7 +1,7 @@
"use client";
import { LikePostList } from "../components";
-import { useUserLikedPost } from "@/lib/hooks/query/useUserQuery";
+import { useUserLikedPost } from "@/client/hooks/query/useUserQuery";
export default function LikePostListLoader() {
const { data: likedPosts, error } = useUserLikedPost();
diff --git a/src/components/domains/user/sections/ActivitySection/loader/MyCommentsListLoader.tsx b/src/components/domains/user/sections/ActivitySection/loader/MyCommentsListLoader.tsx
index 78683655..3fbc2edf 100644
--- a/src/components/domains/user/sections/ActivitySection/loader/MyCommentsListLoader.tsx
+++ b/src/components/domains/user/sections/ActivitySection/loader/MyCommentsListLoader.tsx
@@ -1,7 +1,7 @@
"use client";
import { MyCommentsList } from "../components";
-import { useUserComment } from "@/lib/hooks/query/useUserQuery";
+import { useUserComment } from "@/client/hooks/query/useUserQuery";
export default function MyCommentsListLoader() {
const { data: paginatedComments, error } = useUserComment();
diff --git a/src/components/domains/user/sections/ActivitySection/loader/MyPostListLoader.tsx b/src/components/domains/user/sections/ActivitySection/loader/MyPostListLoader.tsx
index 9f2c1904..fbd85f85 100644
--- a/src/components/domains/user/sections/ActivitySection/loader/MyPostListLoader.tsx
+++ b/src/components/domains/user/sections/ActivitySection/loader/MyPostListLoader.tsx
@@ -1,7 +1,7 @@
"use client";
import { MyPostList } from "../components";
-import { useUserPost } from "@/lib/hooks/query/useUserQuery";
+import { useUserPost } from "@/client/hooks/query/useUserQuery";
export default function MyPostListLoader() {
const { data: paginatedPosts, error } = useUserPost();
diff --git a/src/components/domains/user/sections/ActivitySection/loader/MyReviewListLoader.tsx b/src/components/domains/user/sections/ActivitySection/loader/MyReviewListLoader.tsx
index 732bb178..2c58b896 100644
--- a/src/components/domains/user/sections/ActivitySection/loader/MyReviewListLoader.tsx
+++ b/src/components/domains/user/sections/ActivitySection/loader/MyReviewListLoader.tsx
@@ -2,7 +2,7 @@
import { useEffect, useState } from "react";
import { MyReviewList } from "../components";
-import { useUserReview } from "@/lib/hooks/query/useUserQuery";
+import { useUserReview } from "@/client/hooks/query/useUserQuery";
export default function MyReviewListLoader() {
const [isMounted, setIsMounted] = useState(false);
diff --git a/src/components/domains/user/sections/BookmarkSection/components/CommunityBookmarkList.tsx b/src/components/domains/user/sections/BookmarkSection/components/CommunityBookmarkList.tsx
index 77bf3ec0..a781df26 100644
--- a/src/components/domains/user/sections/BookmarkSection/components/CommunityBookmarkList.tsx
+++ b/src/components/domains/user/sections/BookmarkSection/components/CommunityBookmarkList.tsx
@@ -1,7 +1,7 @@
import Link from "next/link";
import { PostCard } from "@/components/commons/card/postCard";
-import { POST_CARD_TYPES } from "@/lib/constants/post";
-import type { ApiPostResponse } from "@/lib/hono/schemas/community.schema";
+import { POST_CARD_TYPES } from "@/shared/constants/post";
+import type { ApiPostResponse } from "@/server/hono/schemas/community.schema";
export const CommunityBookmarkList = ({
communityPosts,
diff --git a/src/components/domains/user/sections/BookmarkSection/components/PerfumeBookmarkList.tsx b/src/components/domains/user/sections/BookmarkSection/components/PerfumeBookmarkList.tsx
index 1ce7a547..483625b1 100644
--- a/src/components/domains/user/sections/BookmarkSection/components/PerfumeBookmarkList.tsx
+++ b/src/components/domains/user/sections/BookmarkSection/components/PerfumeBookmarkList.tsx
@@ -1,5 +1,5 @@
import PerfumeCard from "@/components/commons/card/perfumeCard";
-import type { ApiPerfumeSimpleResponse } from "@/lib/hono/schemas/perfume.schema";
+import type { ApiPerfumeSimpleResponse } from "@/server/hono/schemas/perfume.schema";
import Link from "next/link";
diff --git a/src/components/domains/user/sections/BookmarkSection/index.tsx b/src/components/domains/user/sections/BookmarkSection/index.tsx
index 9749aff3..68e0f1f0 100644
--- a/src/components/domains/user/sections/BookmarkSection/index.tsx
+++ b/src/components/domains/user/sections/BookmarkSection/index.tsx
@@ -5,7 +5,7 @@ import dynamic from "next/dynamic";
import { SubTabSwitcher } from "@/components/domains/user/tabs/SubTabs";
import { useUrlTabs } from "../../useUrlTabs";
import { SkeletonCard, SkeletonPerfume } from "../../components/skeletons";
-import { useMediaQuery } from "@/lib/hooks/useMediaQuery";
+import { useMediaQuery } from "@/client/hooks/useMediaQuery";
const PerfumeBookmarksLoader = dynamic(
() => import("./loader/PerfumeBookmarksLoader"),
{
diff --git a/src/components/domains/user/sections/BookmarkSection/loader/CommunityBookmarksLoader.tsx b/src/components/domains/user/sections/BookmarkSection/loader/CommunityBookmarksLoader.tsx
index 8cf0da65..cbea58fd 100644
--- a/src/components/domains/user/sections/BookmarkSection/loader/CommunityBookmarksLoader.tsx
+++ b/src/components/domains/user/sections/BookmarkSection/loader/CommunityBookmarksLoader.tsx
@@ -1,7 +1,7 @@
"use client";
import { CommunityBookmarkList } from "../components";
-import { useUserPostsBookmarks } from "@/lib/hooks/query/useUserQuery";
+import { useUserPostsBookmarks } from "@/client/hooks/query/useUserQuery";
export default function CommunityBookmarksLoader() {
const { data: communityData } = useUserPostsBookmarks();
diff --git a/src/components/domains/user/sections/BookmarkSection/loader/PerfumeBookmarksLoader.tsx b/src/components/domains/user/sections/BookmarkSection/loader/PerfumeBookmarksLoader.tsx
index 91f508ba..9166b1a3 100644
--- a/src/components/domains/user/sections/BookmarkSection/loader/PerfumeBookmarksLoader.tsx
+++ b/src/components/domains/user/sections/BookmarkSection/loader/PerfumeBookmarksLoader.tsx
@@ -1,7 +1,7 @@
"use client";
import { PerfumeBookmarkList } from "../components";
-import { usePerfumeBookmarks } from "@/lib/hooks/query/useUserQuery";
+import { usePerfumeBookmarks } from "@/client/hooks/query/useUserQuery";
export default function PerfumeBookmarksLoader({ userId }: { userId: string }) {
const { data: perfumeData } = usePerfumeBookmarks(userId);
diff --git a/src/components/domains/user/sections/CollectionSection/index.tsx b/src/components/domains/user/sections/CollectionSection/index.tsx
index 44e2b544..6ace947e 100644
--- a/src/components/domains/user/sections/CollectionSection/index.tsx
+++ b/src/components/domains/user/sections/CollectionSection/index.tsx
@@ -3,9 +3,9 @@
import Image from "next/image";
import ImageDetailModal from "@/components/modal/imageDetailModal";
import { useImageDetailModal } from "@/components/modal/imageDetailModal/useImageDetailModal";
-import { useUserCollections } from "@/lib/hooks/query/useUserQuery";
+import { useUserCollections } from "@/client/hooks/query/useUserQuery";
import { SkeletonMasonry } from "../../components/skeletons";
-import type { ApiMyCollectionResponse } from "@/lib/hono/schemas/me.schema";
+import type { ApiMyCollectionResponse } from "@/server/hono/schemas/me.schema";
export const CollectionSection = ({ userId }: { userId: string }) => {
const {
diff --git a/src/components/domains/user/sections/ProfileHeaderSection/components/UserHeaderDisplay.tsx b/src/components/domains/user/sections/ProfileHeaderSection/components/UserHeaderDisplay.tsx
index 48a5b409..c61a6ed9 100644
--- a/src/components/domains/user/sections/ProfileHeaderSection/components/UserHeaderDisplay.tsx
+++ b/src/components/domains/user/sections/ProfileHeaderSection/components/UserHeaderDisplay.tsx
@@ -1,4 +1,4 @@
-import type { ApiMyProfileResponse } from "@/lib/hono/schemas/me.schema";
+import type { ApiMyProfileResponse } from "@/server/hono/schemas/me.schema";
import UserProfileImage from "./UserProfileImage";
import UserInfo from "./UserInfo";
diff --git a/src/components/domains/user/sections/ProfileHeaderSection/components/UserInfo.tsx b/src/components/domains/user/sections/ProfileHeaderSection/components/UserInfo.tsx
index e6e62f41..713c35f1 100644
--- a/src/components/domains/user/sections/ProfileHeaderSection/components/UserInfo.tsx
+++ b/src/components/domains/user/sections/ProfileHeaderSection/components/UserInfo.tsx
@@ -1,5 +1,5 @@
import LevelChip from "@/components/commons/chip/LevelChip";
-import type { ApiMyProfileResponse } from "@/lib/hono/schemas/me.schema";
+import type { ApiMyProfileResponse } from "@/server/hono/schemas/me.schema";
interface UserInfoProps {
user: ApiMyProfileResponse;
diff --git a/src/components/domains/user/sections/ProfileHeaderSection/index.tsx b/src/components/domains/user/sections/ProfileHeaderSection/index.tsx
index b0a3ab63..36813131 100644
--- a/src/components/domains/user/sections/ProfileHeaderSection/index.tsx
+++ b/src/components/domains/user/sections/ProfileHeaderSection/index.tsx
@@ -2,11 +2,11 @@
import UserHeaderSkeleton from "./components/UserHeaderSkeleton";
import UserHeaderDisplay from "./components/UserHeaderDisplay";
-import { useUserStore } from "@/lib/stores/useUserStore";
-import type { ApiMyProfileResponse } from "@/lib/hono/schemas/me.schema";
+import { useCurrentUser } from "@/components/commons/Provider/CurrentUserProvider";
+import type { ApiMyProfileResponse } from "@/server/hono/schemas/me.schema";
const UserHeader = ({ user }: { user: ApiMyProfileResponse }) => {
- const isLoading = useUserStore((state) => state.isLoading);
+ const { isLoading } = useCurrentUser();
if (!user || isLoading) {
return
;
diff --git a/src/components/domains/user/sections/ProfileSection/components/PersonalInfo/field/AgeField.tsx b/src/components/domains/user/sections/ProfileSection/components/PersonalInfo/field/AgeField.tsx
index 3724a1db..a473e92f 100644
--- a/src/components/domains/user/sections/ProfileSection/components/PersonalInfo/field/AgeField.tsx
+++ b/src/components/domains/user/sections/ProfileSection/components/PersonalInfo/field/AgeField.tsx
@@ -1,4 +1,4 @@
-import type { ApiMyProfileResponse } from "@/lib/hono/schemas/me.schema";
+import type { ApiMyProfileResponse } from "@/server/hono/schemas/me.schema";
import { Controller, useFormContext } from "react-hook-form";
import { ProfileRow } from "../common/ProfileRow";
import Dropdown from "@/components/commons/dropdown/DropdownBase";
diff --git a/src/components/domains/user/sections/ProfileSection/components/PersonalInfo/field/GenderField.tsx b/src/components/domains/user/sections/ProfileSection/components/PersonalInfo/field/GenderField.tsx
index 313ef135..d1c81900 100644
--- a/src/components/domains/user/sections/ProfileSection/components/PersonalInfo/field/GenderField.tsx
+++ b/src/components/domains/user/sections/ProfileSection/components/PersonalInfo/field/GenderField.tsx
@@ -1,7 +1,7 @@
import { Controller, useFormContext } from "react-hook-form";
import { ProfileRow } from "../common/ProfileRow";
import Dropdown from "@/components/commons/dropdown/DropdownBase";
-import type { ApiMyProfileResponse } from "@/lib/hono/schemas/me.schema";
+import type { ApiMyProfileResponse } from "@/server/hono/schemas/me.schema";
export const GenderField = () => {
const { control } = useFormContext
();
diff --git a/src/components/domains/user/sections/ProfileSection/components/PersonalInfo/field/NicknameField.tsx b/src/components/domains/user/sections/ProfileSection/components/PersonalInfo/field/NicknameField.tsx
index d932e2a9..3fb8f72a 100644
--- a/src/components/domains/user/sections/ProfileSection/components/PersonalInfo/field/NicknameField.tsx
+++ b/src/components/domains/user/sections/ProfileSection/components/PersonalInfo/field/NicknameField.tsx
@@ -1,4 +1,4 @@
-import type { ApiMyProfileResponse } from "@/lib/hono/schemas/me.schema";
+import type { ApiMyProfileResponse } from "@/server/hono/schemas/me.schema";
import { Controller, useFormContext } from "react-hook-form";
import { ProfileRow } from "../common/ProfileRow";
import InputBase from "@/components/commons/input";
diff --git a/src/components/domains/user/sections/ProfileSection/components/PersonalInfo/index.tsx b/src/components/domains/user/sections/ProfileSection/components/PersonalInfo/index.tsx
index dd180ee5..3629f795 100644
--- a/src/components/domains/user/sections/ProfileSection/components/PersonalInfo/index.tsx
+++ b/src/components/domains/user/sections/ProfileSection/components/PersonalInfo/index.tsx
@@ -1,42 +1,49 @@
"use client";
+import { signOut } from "next-auth/react";
+import { useQueryClient } from "@tanstack/react-query";
import { ProfileActions } from "./common/ProfileActions";
import { ProfileForm } from "./common/ProfileForm";
import {
ApiMyProfileResponse,
- ApiMyProfileResponseSchema,
-} from "@/lib/hono/schemas/me.schema";
+ ApiUpdateMyProfileRequest,
+ ApiUpdateMyProfileRequestSchema,
+} from "@/server/hono/schemas/me.schema";
import Form from "@/components/commons/form";
-import { useUpdateProfile } from "@/lib/hooks/query/useUserQuery";
+import {
+ useDeleteAccount,
+ useUpdateProfile,
+} from "@/client/hooks/query/useUserQuery";
export const PersonalInfo = (user: ApiMyProfileResponse) => {
const { mutateAsync, isPending } = useUpdateProfile();
- const defaultValues = {
- id: user.id,
+ const { mutateAsync: deleteAccount, isPending: isDeleting } =
+ useDeleteAccount();
+ const queryClient = useQueryClient();
+
+ const defaultValues: ApiUpdateMyProfileRequest = {
nickname: user.nickname || "",
- age: user.age || 0,
- gender: user.gender || "UNKNOWN",
- imageUrl: user.imageUrl || "",
+ age: user.age ?? undefined,
+ gender: user.gender ?? undefined,
+ imageUrl: user.imageUrl ?? undefined,
};
- // ํํด ์ฒ๋ฆฌ ํจ์
- const handleWithdraw = () => {
- console.log("ํํด ๋ฒํผ ํด๋ฆญ");
- if (confirm("์ ๋ง๋ก ํํดํ์๊ฒ ์ต๋๊น?")) {
- alert("ํํด ์ฒ๋ฆฌ๋์์ต๋๋ค.");
- //TODO: ์ดํ ๋ฉ์ธ์ผ๋ก ์ฒ๋ฆฌ
+ const handleWithdraw = async () => {
+ if (!confirm("์ ๋ง๋ก ํํดํ์๊ฒ ์ต๋๊น? ์ด ์์
์ ๋๋๋ฆด ์ ์์ต๋๋ค."))
+ return;
+ try {
+ await deleteAccount();
+ queryClient.clear();
+ await signOut({ redirectTo: "/" });
+ } catch {
+ alert("ํํด ์ฒ๋ฆฌ ์ค ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค. ๋ค์ ์๋ํด์ฃผ์ธ์.");
}
};
- const onSubmit = async (formData: ApiMyProfileResponse) => {
- const { imageUrl, ...rest } = formData;
- const params = {
- ...rest,
- imageUrl: imageUrl ?? undefined,
- };
+ const onSubmit = async (formData: ApiUpdateMyProfileRequest) => {
try {
- await mutateAsync(params);
+ await mutateAsync(formData);
alert("์์ ์ด ์๋ฃ๋์์ต๋๋ค.");
} catch (error) {
console.error("์ ์ถ ์คํจ:", error);
@@ -44,13 +51,16 @@ export const PersonalInfo = (user: ApiMyProfileResponse) => {
};
return (
-
);
};
diff --git a/src/components/domains/user/sections/ProfileSection/components/ProfileImage/ImageDisplay.tsx b/src/components/domains/user/sections/ProfileSection/components/ProfileImage/ImageDisplay.tsx
index d2300081..b04ab284 100644
--- a/src/components/domains/user/sections/ProfileSection/components/ProfileImage/ImageDisplay.tsx
+++ b/src/components/domains/user/sections/ProfileSection/components/ProfileImage/ImageDisplay.tsx
@@ -1,5 +1,7 @@
import Image from "next/image";
-import ICONS from "@/lib/constants/icons";
+import ICONS from "@/shared/constants/icons";
+import { DEFAULT_PROFILE_IMAGE } from "@/components/commons/author/author.constants";
+
interface ImageDisplayProps {
imageUrl?: string | null;
previewUrl?: string | null;
@@ -11,22 +13,19 @@ export const ImageDisplay = ({
previewUrl,
onEditClick,
}: ImageDisplayProps) => {
- const displaySrc = previewUrl || imageUrl;
+ const displaySrc = previewUrl || imageUrl || DEFAULT_PROFILE_IMAGE;
return (
- {/* ํ๋กํ ์ด๋ฏธ์ง */}
- {displaySrc && (
-
- )}
+
{/* ์์ ๋ฒํผ */}
diff --git a/src/components/domains/user/sections/ProfileSection/index.tsx b/src/components/domains/user/sections/ProfileSection/index.tsx
index 203dbca5..618f4c92 100644
--- a/src/components/domains/user/sections/ProfileSection/index.tsx
+++ b/src/components/domains/user/sections/ProfileSection/index.tsx
@@ -3,15 +3,16 @@
import { useState, useCallback, useEffect } from "react";
import { PersonalInfo } from "./components/PersonalInfo";
import { ProfileImage } from "./components/ProfileImage";
-import { useUploadProfileImage, useUserProfile } from "@/lib/hooks/query/useUserQuery";
+import { useUploadProfileImage } from "@/client/hooks/query/useUserQuery";
+import { useCurrentUser } from "@/components/commons/Provider/CurrentUserProvider";
import { ProfileSectionSkeleton } from "./components/ProfileSectionSkeleton";
-import { PROFILE_BUCKET_NAME } from "@/lib/constants/buckets";
+import { PROFILE_BUCKET_NAME } from "@/shared/constants/buckets";
export const ProfileSection = () => {
const [selectedFile, setSelectedFile] = useState
(null);
const [previewUrl, setPreviewUrl] = useState(null);
- const { data: user, isLoading } = useUserProfile();
+ const { user, isLoading } = useCurrentUser();
const uploadProfileImageMutation = useUploadProfileImage();
const resetState = useCallback(() => {
@@ -27,7 +28,7 @@ export const ProfileSection = () => {
}
setPreviewUrl(URL.createObjectURL(file));
},
- [previewUrl]
+ [previewUrl],
);
const onCancel = () => {
@@ -42,7 +43,7 @@ export const ProfileSection = () => {
onSuccess: () => {
resetState();
},
- }
+ },
);
}
};
diff --git a/src/components/domains/user/sections/RecentViewSection/hooks/useRecentPerfumes.ts b/src/components/domains/user/sections/RecentViewSection/hooks/useRecentPerfumes.ts
index 09b58bd4..a2b78ce5 100644
--- a/src/components/domains/user/sections/RecentViewSection/hooks/useRecentPerfumes.ts
+++ b/src/components/domains/user/sections/RecentViewSection/hooks/useRecentPerfumes.ts
@@ -1,5 +1,5 @@
import { useMemo } from "react";
-import { useRecentPerfumesStore } from "@/lib/stores/useRecentPerfumesStore";
+import { useRecentPerfumesStore } from "@/client/stores/perfumeStore";
import { useFooterPagination } from "./useFooterPagination";
const PERFUMES_PER_VIEW = 5;
diff --git a/src/components/domains/user/sections/RecentViewSection/hooks/useRecentPosts.ts b/src/components/domains/user/sections/RecentViewSection/hooks/useRecentPosts.ts
index 3c65b225..381e0949 100644
--- a/src/components/domains/user/sections/RecentViewSection/hooks/useRecentPosts.ts
+++ b/src/components/domains/user/sections/RecentViewSection/hooks/useRecentPosts.ts
@@ -1,7 +1,7 @@
import { useMemo } from "react";
-import { useRecentPostsStore } from "@/lib/stores/useRecentPostsStore";
+import { useRecentPostsStore } from "@/client/stores/perfumeStore";
import { useFooterPagination } from "./useFooterPagination";
-import type { ApiPostDetailResponse } from "@/lib/hono/schemas/community.schema";
+import type { ApiPostDetailResponse } from "@/server/hono/schemas/community.schema";
interface PostForCard extends Omit {
userId: string;
diff --git a/src/components/domains/user/sections/RecentViewSection/index.tsx b/src/components/domains/user/sections/RecentViewSection/index.tsx
index 5d41ab90..6c03cd34 100644
--- a/src/components/domains/user/sections/RecentViewSection/index.tsx
+++ b/src/components/domains/user/sections/RecentViewSection/index.tsx
@@ -1,6 +1,6 @@
"use client";
-import { useMediaQuery } from "@/lib/hooks/useMediaQuery";
+import { useMediaQuery } from "@/client/hooks/useMediaQuery";
import { RecentPostsSection } from "./components/RecentPostsSection";
import { RecentPerfumesSection } from "./components/RecentPerfumesSection";
diff --git a/src/components/domains/user/sections/sections.type.ts b/src/components/domains/user/sections/sections.type.ts
index ad30957f..fb354637 100644
--- a/src/components/domains/user/sections/sections.type.ts
+++ b/src/components/domains/user/sections/sections.type.ts
@@ -1,11 +1,11 @@
-import type { CommentResponse } from "@/lib/hono/schemas/comment.schema";
-import type { ApiPostResponse } from "@/lib/hono/schemas/community.schema";
-import type { ApiReviewResponse } from "@/lib/hono/schemas/review.schema";
+import type { CommentResponse } from "@/server/hono/schemas/comment.schema";
+import type { ApiPostResponse } from "@/server/hono/schemas/community.schema";
+import type { ApiReviewResponse } from "@/server/hono/schemas/review.schema";
import {
ApiMyCollectionResponse,
ApiMyProfileResponse,
ApiMyBookmarkedPerfumesResponse,
-} from "@/lib/hono/schemas/me.schema";
+} from "@/server/hono/schemas/me.schema";
export type BookmarkData = {
perfumes: ApiMyBookmarkedPerfumesResponse[];
diff --git a/src/components/domains/user/tabs/MainTabs.tsx b/src/components/domains/user/tabs/MainTabs.tsx
index b0aea3f4..215b206f 100644
--- a/src/components/domains/user/tabs/MainTabs.tsx
+++ b/src/components/domains/user/tabs/MainTabs.tsx
@@ -1,7 +1,7 @@
import React from "react";
import Link from "next/link";
import { getVisibleTabs } from "./tabs.helper";
-import { useUserStore } from "@/lib/stores/useUserStore";
+import { useCurrentUser } from "@/components/commons/Provider/CurrentUserProvider";
import { MobileTabsAccordion } from "./MobileTabsAccordion";
import { FloatingActionButton } from "../components/FAB";
@@ -18,7 +18,7 @@ const MainTabs = ({
onAddPhotoClick: () => void;
children?: React.ReactNode;
}) => {
- const user = useUserStore((state) => state.user);
+ const { user } = useCurrentUser();
const tabItems = getVisibleTabs(isMe, user?.nickname);
const isCollectionTab = isMe && tab === "collection";
diff --git a/src/components/domains/user/tabs/MobileTabsAccordion.tsx b/src/components/domains/user/tabs/MobileTabsAccordion.tsx
index f4c674c7..fd9b504f 100644
--- a/src/components/domains/user/tabs/MobileTabsAccordion.tsx
+++ b/src/components/domains/user/tabs/MobileTabsAccordion.tsx
@@ -3,8 +3,8 @@
import React, { useEffect, useState } from "react";
import Link from "next/link";
import Image from "next/image";
-import ICONS from "@/lib/constants/icons";
-import { useUserStore } from "@/lib/stores/useUserStore";
+import ICONS from "@/shared/constants/icons";
+import { useCurrentUser } from "@/components/commons/Provider/CurrentUserProvider";
import { TabItem } from "./tabs.type";
interface MobileTabsAccordionProps {
@@ -18,7 +18,7 @@ export const MobileTabsAccordion = ({
currentTab,
children,
}: MobileTabsAccordionProps) => {
- const user = useUserStore((state) => state.user);
+ const { user } = useCurrentUser();
const [isOpen, setIsOpen] = useState(true);
diff --git a/src/components/modal/AccountLinkedModal.tsx b/src/components/modal/AccountLinkedModal.tsx
new file mode 100644
index 00000000..708f21f1
--- /dev/null
+++ b/src/components/modal/AccountLinkedModal.tsx
@@ -0,0 +1,151 @@
+"use client";
+
+import { useState } from "react";
+import { signIn } from "next-auth/react";
+import { ModalContainer } from "./ModalContainer";
+import Image from "next/image";
+import IMAGES from "@/shared/constants/images";
+
+const PROVIDER_META: Record<
+ string,
+ { label: string; imageKey: string; width: number; height: number }
+> = {
+ google: { label: "๊ตฌ๊ธ", imageKey: "Google", width: 32, height: 32 },
+ naver: { label: "๋ค์ด๋ฒ", imageKey: "Naver", width: 48, height: 48 },
+ kakao: { label: "์นด์นด์ค", imageKey: "Kakao", width: 32, height: 32 },
+};
+
+interface AccountLinkedModalProps {
+ provider: string;
+ mode: "confirm" | "linked";
+ token?: string;
+ closeModal: () => void;
+}
+
+export const AccountLinkedModal = ({
+ provider,
+ mode,
+ token,
+ closeModal,
+}: AccountLinkedModalProps) => {
+ const [isPending, setIsPending] = useState(false);
+ const [error, setError] = useState("");
+
+ const meta = PROVIDER_META[provider];
+
+ const handleConfirm = async () => {
+ if (!token) return;
+ setIsPending(true);
+ setError("");
+
+ try {
+ const res = await fetch("/api/v1/auth/confirm-link", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ token }),
+ });
+
+ if (!res.ok) {
+ const json = await res.json().catch(() => ({}));
+ setError(json.message ?? "์ฐ๊ฒฐ์ ์คํจํ์ต๋๋ค. ๋ค์ ์๋ํด ์ฃผ์ธ์.");
+ return;
+ }
+
+ // ์ฐ๊ฒฐ ์๋ฃ ํ ํด๋น provider๋ก ์ฌ๋ก๊ทธ์ธ
+ await signIn(provider, { callbackUrl: "/" });
+ } catch {
+ setError("์ฐ๊ฒฐ์ ์คํจํ์ต๋๋ค. ๋ค์ ์๋ํด ์ฃผ์ธ์.");
+ } finally {
+ setIsPending(false);
+ }
+ };
+
+ if (mode === "confirm") {
+ return (
+
+
+
+ {meta && (
+
+ )}
+
+
+ ์ด๋ฏธ ๊ฐ์
๋ ์ด๋ฉ์ผ์ด์์
+
+
+ ๊ฐ์ ์ด๋ฉ์ผ๋ก ๊ฐ์
๋ ๊ณ์ ์ด ์์ด์.
+
+ {meta
+ ? `${meta.label} ๊ณ์ ์ ๊ธฐ์กด ๊ณ์ ์ ์ฐ๊ฒฐํ์๊ฒ ์ด์?`
+ : "์ด ์์
๊ณ์ ์ ๊ธฐ์กด ๊ณ์ ์ ์ฐ๊ฒฐํ์๊ฒ ์ด์?"}
+
+
+
+
+ {error && (
+
{error}
+ )}
+
+
+
+
+
+
+
+ );
+ }
+
+ // mode === "linked" (์ค์ ํ์ด์ง์์ ์ฐ๊ฒฐ ์๋ฃ ํ ์๋ฆผ์ฉ์ผ๋ก ์ ์ง)
+ return (
+
+
+
+ {meta && (
+
+ )}
+
+
+ ๊ธฐ์กด ๊ณ์ ์ ์ฐ๊ฒฐ๋์์ด์
+
+
+ ์ด๋ฏธ ๊ฐ์ ์ด๋ฉ์ผ๋ก ๊ฐ์
๋ ๊ณ์ ์ด ์์ด์.
+
+ {meta
+ ? `${meta.label} ๊ณ์ ์ด ๊ธฐ์กด ๊ณ์ ์ ์ฐ๊ฒฐ๋์์ต๋๋ค.`
+ : "์์
๊ณ์ ์ด ๊ธฐ์กด ๊ณ์ ์ ์ฐ๊ฒฐ๋์์ต๋๋ค."}
+
+
+
+
+
+
+
+ );
+};
diff --git a/src/components/modal/LoginModal.tsx b/src/components/modal/LoginModal.tsx
index 92cad592..1b8c0b34 100644
--- a/src/components/modal/LoginModal.tsx
+++ b/src/components/modal/LoginModal.tsx
@@ -4,10 +4,10 @@ import {
googleLogin,
kakaoLogin,
naverLogin,
-} from "@/lib/database/action/login";
+} from "@/server/database/action/login";
import { ModalContainer } from "./ModalContainer";
import Image from "next/image";
-import IMAGES from "@/lib/constants/images";
+import IMAGES from "@/shared/constants/images";
import { SocialLogoContainer } from "../commons/socialLogo/SocialLogoContainer";
import { usePathname, useSearchParams } from "next/navigation";
@@ -66,7 +66,7 @@ export const LoginModal = ({ closeModal }: ILoginModalProps) => {
- {/* kakao: email ๋ฐ์์ค๋ ๊ถํ ์์. ๋ณด๋ฅ */}
+ {/* kakao */}
);
diff --git a/src/components/modal/NicknameSetupModal.tsx b/src/components/modal/NicknameSetupModal.tsx
new file mode 100644
index 00000000..ec073c60
--- /dev/null
+++ b/src/components/modal/NicknameSetupModal.tsx
@@ -0,0 +1,131 @@
+"use client";
+
+import { useState } from "react";
+import { useSession } from "next-auth/react";
+import { ModalContainer } from "./ModalContainer";
+import { meApi } from "@/client/utils/api/users.api";
+import { useQueryClient } from "@tanstack/react-query";
+import { queryKeys } from "@/client/utils/queryKeys";
+
+interface NicknameSetupModalProps {
+ defaultNickname: string;
+ closeModal: () => void;
+}
+
+export const NicknameSetupModal = ({
+ defaultNickname,
+ closeModal,
+}: NicknameSetupModalProps) => {
+ const { update } = useSession();
+ const queryClient = useQueryClient();
+ const [nickname, setNickname] = useState("");
+ const [error, setError] = useState("");
+ const [isPending, setIsPending] = useState(false);
+
+ const DEFAULT_NICKNAME_NOTICE =
+ "๋๋ค์์ ์์ ํ์ง ์์ผ๋ฉด ๊ธฐ๋ณธ๊ฐ์ด ์ฌ์ฉ๋ฉ๋๋ค.";
+ const handleClose = async () => {
+ try {
+ if (defaultNickname) {
+ await applyNickname(defaultNickname);
+ } else {
+ await update({ isNewUser: false });
+ }
+ } catch {
+ // ์ ์ฅ ์คํจํด๋ ๋ชจ๋ฌ์ ๋ซํ โ ๋ค์ ๋ก๊ทธ์ธ ์ ์ฌ์๋
+ }
+ closeModal();
+ };
+
+ const applyNickname = async (value: string) => {
+ await meApi.profile.update({ nickname: value });
+ await update({ isNewUser: false });
+ queryClient.invalidateQueries({ queryKey: queryKeys.user.profile("me") });
+ };
+
+ const getValidationError = (value: string) => {
+ if (!value) return "";
+ if (value.length < 2)
+ return "๋๋ค์์ ํ๊ธ 2๊ธ์ ์ด์, ์๋ฌธ 3๊ธ์ ์ด์๋ง ์
๋ ฅ ๊ฐ๋ฅํฉ๋๋ค.";
+ return "";
+ };
+
+ const handleNicknameChange = (e: React.ChangeEvent) => {
+ const value = e.target.value;
+ setNickname(value);
+ setError(getValidationError(value));
+ };
+
+ const handleSubmit = async (e: React.FormEvent) => {
+ e.preventDefault();
+ const trimmed = nickname.trim();
+
+ if (!trimmed) {
+ setError("๋๋ค์์ ์
๋ ฅํด์ฃผ์ธ์.");
+ return;
+ }
+ if (trimmed.length < 2 || trimmed.length > 20) {
+ setError("๋๋ค์์ 2์ ์ด์ 20์ ์ดํ๋ก ์
๋ ฅํด์ฃผ์ธ์.");
+ return;
+ }
+
+ setIsPending(true);
+ setError("");
+
+ try {
+ await applyNickname(trimmed);
+ closeModal();
+ } catch {
+ setError("์ด๋ฏธ ์ฌ์ฉ ์ค์ธ ๋๋ค์์
๋๋ค.");
+ } finally {
+ setIsPending(false);
+ }
+ };
+
+ return (
+
+
+
+ ์๋น์ค์์ ์ฌ์ฉํ ๋๋ค์์ ์ค์ ํด์ฃผ์ธ์.
+
+ ๋์ค์ ํ๋กํ์์ ๋ณ๊ฒฝํ ์ ์์ต๋๋ค.
+
+
+
+
+
+ );
+};
diff --git a/src/components/modal/draftRestoreModal/index.tsx b/src/components/modal/draftRestoreModal/index.tsx
index 810706fc..02560e20 100644
--- a/src/components/modal/draftRestoreModal/index.tsx
+++ b/src/components/modal/draftRestoreModal/index.tsx
@@ -1,9 +1,9 @@
"use client";
import { ModalContainer } from "../ModalContainer";
-import { ApiDraftResponse } from "@/lib/hono/schemas/draft.schema";
-import { DraftType } from "@prisma/client";
-import { BOARD_OPTIONS } from "@/lib/constants/options";
+import { ApiDraftResponse } from "@/server/hono/schemas/draft.schema";
+import { DraftType } from "@/server/hono/schemas/draft.schema";
+import { BOARD_OPTIONS } from "@/shared/constants/options";
import dayjs from "dayjs";
import relativeTime from "dayjs/plugin/relativeTime";
import "dayjs/locale/ko";
diff --git a/src/components/modal/photocollectionUploadModal/PerfumeTagger.tsx b/src/components/modal/photocollectionUploadModal/PerfumeTagger.tsx
index df39b17c..890c7a8c 100644
--- a/src/components/modal/photocollectionUploadModal/PerfumeTagger.tsx
+++ b/src/components/modal/photocollectionUploadModal/PerfumeTagger.tsx
@@ -1,7 +1,7 @@
import React, { useState } from "react";
import { X } from "lucide-react";
import { useSearchTag } from "./useSearchTag";
-import type { ApiPerfumeSimpleResponse } from "@/lib/hono/schemas/perfume.schema";
+import type { ApiPerfumeSimpleResponse } from "@/server/hono/schemas/perfume.schema";
import Image from "next/image";
interface PerfumeTaggerProps {
diff --git a/src/components/modal/photocollectionUploadModal/index.tsx b/src/components/modal/photocollectionUploadModal/index.tsx
index 811b5a1c..cf2364ce 100644
--- a/src/components/modal/photocollectionUploadModal/index.tsx
+++ b/src/components/modal/photocollectionUploadModal/index.tsx
@@ -1,11 +1,11 @@
import React, { useCallback, useEffect, useState } from "react";
-import ModalPortal from "@/lib/portal/ModalPortal";
+import ModalPortal from "@/client/portal/ModalPortal";
import { ModalContainer } from "../ModalContainer";
import { PhotoDropzone } from "./PhotoDropzone";
import { PerfumeTagger } from "./PerfumeTagger";
-import { fileApi } from "@/lib/utils/api/files.api";
-import type { ApiPerfumeSimpleResponse } from "@/lib/hono/schemas/perfume.schema";
-import { useCollectionMutation } from "@/lib/hooks/query/useCollectionQuery";
+import { fileApi } from "@/client/utils/api/files.api";
+import type { ApiPerfumeSimpleResponse } from "@/server/hono/schemas/perfume.schema";
+import { useCollectionMutation } from "@/client/hooks/query/useCollectionQuery";
interface PhotoCollectionUploadModalProps {
isOpen: boolean;
diff --git a/src/components/modal/photocollectionUploadModal/useSearchTag.ts b/src/components/modal/photocollectionUploadModal/useSearchTag.ts
index 9c411a49..fb1a9dea 100644
--- a/src/components/modal/photocollectionUploadModal/useSearchTag.ts
+++ b/src/components/modal/photocollectionUploadModal/useSearchTag.ts
@@ -1,6 +1,6 @@
import { useState, useEffect } from "react";
-import { useCollectionPerfumeSearch } from "@/lib/hooks/query/useCollectionQuery";
-import type { ApiPerfumeSimpleResponse } from "@/lib/hono/schemas/perfume.schema";
+import { useCollectionPerfumeSearch } from "@/client/hooks/query/useCollectionQuery";
+import type { ApiPerfumeSimpleResponse } from "@/server/hono/schemas/perfume.schema";
const initialState = { data: [] as ApiPerfumeSimpleResponse[] };
diff --git a/src/components/modal/reviewModal/components/button/ImageButton.tsx b/src/components/modal/reviewModal/components/button/ImageButton.tsx
index bfad3af8..47a5b6a7 100644
--- a/src/components/modal/reviewModal/components/button/ImageButton.tsx
+++ b/src/components/modal/reviewModal/components/button/ImageButton.tsx
@@ -1,5 +1,5 @@
import Image from "next/image";
-import { ReviewImageType } from "@/lib/types/review.types";
+import { ReviewImageType } from "@/shared/types/review.types";
interface IImageButton {
image: ReviewImageType;
diff --git a/src/components/modal/reviewModal/components/button/RadioButton.tsx b/src/components/modal/reviewModal/components/button/RadioButton.tsx
index 0398cbad..eccf7112 100644
--- a/src/components/modal/reviewModal/components/button/RadioButton.tsx
+++ b/src/components/modal/reviewModal/components/button/RadioButton.tsx
@@ -1,4 +1,4 @@
-import ICONS from "@/lib/constants/icons";
+import ICONS from "@/shared/constants/icons";
import Image from "next/image";
interface IRadioButton {
diff --git a/src/components/modal/reviewModal/components/button/SelectButton.tsx b/src/components/modal/reviewModal/components/button/SelectButton.tsx
index 12df0bd0..539b1315 100644
--- a/src/components/modal/reviewModal/components/button/SelectButton.tsx
+++ b/src/components/modal/reviewModal/components/button/SelectButton.tsx
@@ -1,4 +1,4 @@
-import ICONS from "@/lib/constants/icons";
+import ICONS from "@/shared/constants/icons";
import Image from "next/image";
interface ISelectButton {
diff --git a/src/components/modal/reviewModal/components/sections/FeelingSection.tsx b/src/components/modal/reviewModal/components/sections/FeelingSection.tsx
index 0e2ae221..935bffd7 100644
--- a/src/components/modal/reviewModal/components/sections/FeelingSection.tsx
+++ b/src/components/modal/reviewModal/components/sections/FeelingSection.tsx
@@ -3,7 +3,7 @@
import { Controller, useFormContext } from "react-hook-form";
import { ImageButton } from "../button/ImageButton";
import { SubTitle } from "../SubTitle";
-import { REVIEW_OPTIONS } from "@/lib/constants/review";
+import { REVIEW_OPTIONS } from "@/shared/constants/review";
import { useFormError } from "../../hooks/useFormError";
export const FeelingSection = () => {
diff --git a/src/components/modal/reviewModal/components/sections/GenderToneSection.tsx b/src/components/modal/reviewModal/components/sections/GenderToneSection.tsx
index 18e85ab0..bc94b254 100644
--- a/src/components/modal/reviewModal/components/sections/GenderToneSection.tsx
+++ b/src/components/modal/reviewModal/components/sections/GenderToneSection.tsx
@@ -3,7 +3,7 @@
import { Controller, useFormContext } from "react-hook-form";
import { RadioButton } from "../button/RadioButton";
import { SubTitle } from "../SubTitle";
-import { REVIEW_OPTIONS } from "@/lib/constants/review";
+import { REVIEW_OPTIONS } from "@/shared/constants/review";
import { useFormError } from "../../hooks/useFormError";
export const GenderToneSection = () => {
diff --git a/src/components/modal/reviewModal/components/sections/LongevitySection.tsx b/src/components/modal/reviewModal/components/sections/LongevitySection.tsx
index 9d701741..ec3e42c3 100644
--- a/src/components/modal/reviewModal/components/sections/LongevitySection.tsx
+++ b/src/components/modal/reviewModal/components/sections/LongevitySection.tsx
@@ -1,6 +1,6 @@
"use client";
-import { REVIEW_OPTIONS } from "@/lib/constants/review";
+import { REVIEW_OPTIONS } from "@/shared/constants/review";
import { SelectButton } from "../button/SelectButton";
import { SubTitle } from "../SubTitle";
import { Controller, useFormContext } from "react-hook-form";
diff --git a/src/components/modal/reviewModal/components/sections/PriceSection.tsx b/src/components/modal/reviewModal/components/sections/PriceSection.tsx
index 836edd99..b6043b91 100644
--- a/src/components/modal/reviewModal/components/sections/PriceSection.tsx
+++ b/src/components/modal/reviewModal/components/sections/PriceSection.tsx
@@ -1,7 +1,7 @@
"use client";
import { useFormContext, Controller } from "react-hook-form";
-import { REVIEW_OPTIONS } from "@/lib/constants/review";
+import { REVIEW_OPTIONS } from "@/shared/constants/review";
import { SubTitle } from "../SubTitle";
import { SelectButton } from "../button/SelectButton";
import { useFormError } from "../../hooks/useFormError";
diff --git a/src/components/modal/reviewModal/components/sections/SeasonalSection.tsx b/src/components/modal/reviewModal/components/sections/SeasonalSection.tsx
index cf967f37..818aed65 100644
--- a/src/components/modal/reviewModal/components/sections/SeasonalSection.tsx
+++ b/src/components/modal/reviewModal/components/sections/SeasonalSection.tsx
@@ -1,6 +1,6 @@
"use client";
-import { REVIEW_OPTIONS } from "@/lib/constants/review";
+import { REVIEW_OPTIONS } from "@/shared/constants/review";
import { SubTitle } from "../SubTitle";
import { RadioButton } from "../button/RadioButton";
import { Controller } from "react-hook-form";
diff --git a/src/components/modal/reviewModal/components/sections/SillageSection.tsx b/src/components/modal/reviewModal/components/sections/SillageSection.tsx
index fc3eb98d..f713c99e 100644
--- a/src/components/modal/reviewModal/components/sections/SillageSection.tsx
+++ b/src/components/modal/reviewModal/components/sections/SillageSection.tsx
@@ -1,6 +1,6 @@
"use client";
-import { REVIEW_OPTIONS } from "@/lib/constants/review";
+import { REVIEW_OPTIONS } from "@/shared/constants/review";
import { SelectButton } from "../button/SelectButton";
import { SubTitle } from "../SubTitle";
import { Controller, useFormContext } from "react-hook-form";
diff --git a/src/components/modal/reviewModal/components/sections/StatusSection.tsx b/src/components/modal/reviewModal/components/sections/StatusSection.tsx
index 6567d84a..5fcbc9f0 100644
--- a/src/components/modal/reviewModal/components/sections/StatusSection.tsx
+++ b/src/components/modal/reviewModal/components/sections/StatusSection.tsx
@@ -1,10 +1,10 @@
"use client";
-import { PerfumeUsageStatus } from "@prisma/client";
+import { PerfumeUsageStatus } from "@/server/hono/schemas/review.schema";
import { useFormContext, Controller } from "react-hook-form";
import { SelectButton } from "../button/SelectButton";
import { SubTitle } from "../SubTitle";
-import { REVIEW_OPTIONS } from "@/lib/constants/review";
+import { REVIEW_OPTIONS } from "@/shared/constants/review";
import type { CreateReviewClientInput } from "../../reviewSchema.client";
export const StatusSection = () => {
diff --git a/src/components/modal/reviewModal/components/sections/TimeSection.tsx b/src/components/modal/reviewModal/components/sections/TimeSection.tsx
index f60eb1a5..4ef9445b 100644
--- a/src/components/modal/reviewModal/components/sections/TimeSection.tsx
+++ b/src/components/modal/reviewModal/components/sections/TimeSection.tsx
@@ -1,6 +1,6 @@
"use client";
-import { REVIEW_OPTIONS } from "@/lib/constants/review";
+import { REVIEW_OPTIONS } from "@/shared/constants/review";
import { SubTitle } from "../SubTitle";
import { RadioButton } from "../button/RadioButton";
import { Controller, useFormContext } from "react-hook-form";
diff --git a/src/components/modal/reviewModal/index.tsx b/src/components/modal/reviewModal/index.tsx
index 69d34f16..c7f9088d 100644
--- a/src/components/modal/reviewModal/index.tsx
+++ b/src/components/modal/reviewModal/index.tsx
@@ -14,10 +14,12 @@ import {
DetailReviewSection,
} from "./components/sections";
import { useReviewSubmit } from "./useReviewSubmit";
-import { useUserStore } from "@/lib/stores/useUserStore";
-import { CreateReviewClientSchema, type CreateReviewClientInput } from "./reviewSchema.client";
+import {
+ CreateReviewClientSchema,
+ type CreateReviewClientInput,
+} from "./reviewSchema.client";
import { SubmitButton } from "./components/button/SubmitButton";
-import { PerfumeUsageStatus } from "@prisma/client";
+import { PerfumeUsageStatus } from "@/server/hono/schemas/review.schema";
interface IReviewModalProps {
closeModal: () => void;
@@ -27,8 +29,6 @@ interface IReviewModalProps {
// 1. ๋ฌด์์๋ก ์ ํ๋๋๋๋ก ๋ณด์ฌ์ฃผ๊ธฐ
// 2. ๊ฐ ์น์
์์๋๋ก ๋ณด์ฌ์ฃผ๊ธฐ => tags ๋ด๋ถ์ ์๋ก์ด ๋ฐฐ์ด ํ์ํ ๋ฏ
export const ReviewModal = ({ closeModal }: IReviewModalProps) => {
- const { user } = useUserStore();
-
// ๋ฆฌ๋ทฐ ์ ์ถ ๋ก์ง
const { onSubmit } = useReviewSubmit(closeModal);
@@ -48,11 +48,6 @@ export const ReviewModal = ({ closeModal }: IReviewModalProps) => {
};
}, []);
- if (!user) {
- alert("๋ก๊ทธ์ธ ํ ๋ฆฌ๋ทฐ ์์ฑ์ด ๊ฐ๋ฅํฉ๋๋ค.");
- closeModal();
- return;
- }
return (
void) => {
diff --git a/src/lib/ckeditor/finalizeWithBlobRegistry.ts b/src/lib/ckeditor/finalizeWithBlobRegistry.ts
deleted file mode 100644
index 3912b93d..00000000
--- a/src/lib/ckeditor/finalizeWithBlobRegistry.ts
+++ /dev/null
@@ -1,31 +0,0 @@
-import type { BlobRegistry } from "@/lib/ckeditor/localPreviewUploadPlugin";
-import { fileApi } from "@/lib/utils/api/files.api";
-import { POST_IMAGES_BUCKET_NAME } from "@/lib/constants/buckets";
-
-export async function finalizeWithBlobRegistry(
- html: string,
- registry: BlobRegistry
-) {
- const doc = new DOMParser().parseFromString(html, "text/html");
- const imgs = Array.from(doc.querySelectorAll("img[src^='blob:']"));
-
- for (const img of imgs) {
- const src = img.getAttribute("src")!;
- const file = registry.get(src);
- if (!file) {
- continue;
- }
-
- const result = await fileApi.upload(file, POST_IMAGES_BUCKET_NAME);
-
- if (!result.success || !result.data.imageUrl) {
- throw new Error("์ด๋ฏธ์ง ์
๋ก๋์ ์คํจํ์ต๋๋ค.");
- }
-
- img.setAttribute("src", result.data.imageUrl);
- URL.revokeObjectURL(src);
- registry.delete(src);
- }
-
- return doc.body.innerHTML;
-}
diff --git a/src/lib/ckeditor/localPreviewUploadPlugin.ts b/src/lib/ckeditor/localPreviewUploadPlugin.ts
deleted file mode 100644
index 43955874..00000000
--- a/src/lib/ckeditor/localPreviewUploadPlugin.ts
+++ /dev/null
@@ -1,37 +0,0 @@
-import { Editor, FileLoader } from "ckeditor5";
-
-export type BlobRegistry = Map;
-
-const MAX_FILE_SIZE = 5 * 1024 * 1024;
-
-class LocalPreviewAdapter {
- private loader: FileLoader;
- private registry: BlobRegistry;
-
- constructor(loader: FileLoader, registry: BlobRegistry) {
- this.loader = loader;
- this.registry = registry;
- }
-
- async upload() {
- const file = await this.loader.file;
- if (!file) throw new Error("ํ์ผ์ด ์กด์ฌํ์ง ์์ต๋๋ค.");
- if (file.size > MAX_FILE_SIZE) {
- return Promise.reject("5MB ์ดํ ์ด๋ฏธ์ง ํ์ผ๋ง ์
๋ก๋ํ ์ ์์ต๋๋ค.");
- }
-
- const objectURL = URL.createObjectURL(file);
- this.registry.set(objectURL, file);
- return { default: objectURL };
- }
-
- abort() {}
-}
-
-export default function makeLocalPreviewUploadPlugin(registry: BlobRegistry) {
- return function localPreviewUploadPlugin(editor: Editor) {
- editor.plugins.get("FileRepository").createUploadAdapter = (
- loader: FileLoader
- ) => new LocalPreviewAdapter(loader, registry);
- };
-}
diff --git a/src/lib/ckeditor/supabaseUploadPlugin.ts b/src/lib/ckeditor/supabaseUploadPlugin.ts
deleted file mode 100644
index 7ff065bf..00000000
--- a/src/lib/ckeditor/supabaseUploadPlugin.ts
+++ /dev/null
@@ -1,35 +0,0 @@
-import { Editor, FileLoader } from "ckeditor5";
-import { fileApi } from "@/lib/utils/api/files.api";
-import { POST_IMAGES_BUCKET_NAME } from "@/lib/constants/buckets";
-
-class SupabaseUploadAdapter {
- loader: FileLoader;
- constructor(loader: FileLoader) {
- this.loader = loader;
- }
- async upload() {
- const file = await this.loader.file;
- if (!file) throw new Error("ํ์ผ์ด ์กด์ฌํ์ง ์์ต๋๋ค.");
-
- try {
- const result = await fileApi.upload(file, POST_IMAGES_BUCKET_NAME);
- if (!result.success || !result.data.imageUrl) {
- return Promise.reject("์ด๋ฏธ์ง ์
๋ก๋์ ์คํจํ์ต๋๋ค.");
- }
- return { default: result.data.imageUrl };
- } catch (error) {
- const message =
- error instanceof Error
- ? error.message
- : "์ด๋ฏธ์ง ์
๋ก๋์ ์คํจํ์ต๋๋ค.";
- return Promise.reject(message);
- }
- }
- abort() {}
-}
-
-export default function supabaseUploadPlugin(editor: Editor) {
- editor.plugins.get("FileRepository").createUploadAdapter = (loader) => {
- return new SupabaseUploadAdapter(loader);
- };
-}
diff --git a/src/lib/hono/services/__tests__/brand.service.test.ts b/src/lib/hono/services/__tests__/brand.service.test.ts
deleted file mode 100644
index f34550bf..00000000
--- a/src/lib/hono/services/__tests__/brand.service.test.ts
+++ /dev/null
@@ -1,190 +0,0 @@
-import { describe, it, expect, beforeEach, vi } from "vitest";
-import { prisma } from "@/lib/prisma";
-import {
- getAllBrandsService,
- getBrandByIdService,
- getBrandByNameService,
-} from "../brand.service";
-
-/**
- * Brand ์๋น์ค ํ
์คํธ (MVP)
- *
- * ํ
์คํธ ์ ๋ต:
- * - ๋ธ๋๋ CRUD ํต์ฌ ๋ก์ง ๊ฒ์ฆ
- * - nameKo null ์ nameEn ๋์ฒด ๋ก์ง ํ์ธ
- * - ์๋ฌ ์ฒ๋ฆฌ ๊ฒ์ฆ
- *
- * ์ฃผ์ ์๋๋ฆฌ์ค:
- * 1. ์ ์ฒด ๋ธ๋๋ ๋ชฉ๋ก ์กฐํ
- * 2. ID๋ก ๋ธ๋๋ ์กฐํ (์กด์ฌ/๋ฏธ์กด์ฌ)
- * 3. ์ด๋ฆ์ผ๋ก ๋ธ๋๋ ์กฐํ (์กด์ฌ/๋ฏธ์กด์ฌ)
- * 4. nameKo null ์ nameEn ๋์ฒด
- */
-
-// Mock Prisma
-vi.mock("@/lib/prisma", () => ({
- prisma: {
- brand: {
- findMany: vi.fn(),
- findUnique: vi.fn(),
- },
- },
-}));
-
-vi.mock("../utils/prisma.utils", () => ({
- brandDetailSelect: {
- id: true,
- nameEn: true,
- nameKo: true,
- description: true,
- imageUrl: true,
- brandUrl: true,
- mapLocation: true,
- },
- parseMapLocation: vi.fn((location) => location),
-}));
-
-describe("Brand Service", () => {
- beforeEach(() => {
- vi.clearAllMocks();
- });
-
- describe("getAllBrandsService", () => {
- it("๋ชจ๋ ๋ธ๋๋๋ฅผ ์กฐํํด์ผ ํ๋ค", async () => {
- const mockBrands = [
- {
- id: "brand-1",
- nameEn: "Brand One",
- nameKo: "๋ธ๋๋ ์",
- description: "Description 1",
- imageUrl: "brand1.jpg",
- brandUrl: "https://brand1.com",
- mapLocation: null,
- },
- {
- id: "brand-2",
- nameEn: "Brand Two",
- nameKo: null,
- description: "Description 2",
- imageUrl: "brand2.jpg",
- brandUrl: null,
- mapLocation: null,
- },
- ];
-
- vi.mocked(prisma.brand.findMany).mockResolvedValue(mockBrands as never);
-
- const result = await getAllBrandsService();
-
- expect(result.success).toBe(true);
- if (result.success) {
- expect(result.data).toHaveLength(2);
- expect(result.data[0].nameKo).toBe("๋ธ๋๋ ์");
- expect(result.data[1].nameKo).toBe("Brand Two"); // nameKo๊ฐ null์ด๋ฉด nameEn ์ฌ์ฉ
- }
- });
-
- it("DB ์๋ฌ ์ INTERNAL_ERROR๋ฅผ ๋ฐํํด์ผ ํ๋ค", async () => {
- vi.mocked(prisma.brand.findMany).mockRejectedValue(
- new Error("Database error")
- );
-
- const result = await getAllBrandsService();
-
- expect(result.success).toBe(false);
- if (!result.success) {
- expect(result.error).toBe("INTERNAL_ERROR");
- }
- });
- });
-
- describe("getBrandByIdService", () => {
- it("ID๋ก ๋ธ๋๋๋ฅผ ์กฐํํด์ผ ํ๋ค", async () => {
- const mockBrand = {
- id: "brand-1",
- nameEn: "Test Brand",
- nameKo: "ํ
์คํธ ๋ธ๋๋",
- description: "Test Description",
- imageUrl: "test.jpg",
- brandUrl: "https://test.com",
- mapLocation: null,
- };
-
- vi.mocked(prisma.brand.findUnique).mockResolvedValue(mockBrand as never);
-
- const result = await getBrandByIdService("brand-1");
-
- expect(result.success).toBe(true);
- if (result.success) {
- expect(result.data.id).toBe("brand-1");
- expect(result.data.nameKo).toBe("ํ
์คํธ ๋ธ๋๋");
- }
- });
-
- it("nameKo๊ฐ null์ด๋ฉด nameEn์ ์ฌ์ฉํด์ผ ํ๋ค", async () => {
- const mockBrand = {
- id: "brand-1",
- nameEn: "Test Brand",
- nameKo: null,
- description: "Test Description",
- imageUrl: "test.jpg",
- brandUrl: null,
- mapLocation: null,
- };
-
- vi.mocked(prisma.brand.findUnique).mockResolvedValue(mockBrand as never);
-
- const result = await getBrandByIdService("brand-1");
-
- expect(result.success).toBe(true);
- if (result.success) {
- expect(result.data.nameKo).toBe("Test Brand");
- }
- });
-
- it("์กด์ฌํ์ง ์๋ ๋ธ๋๋๋ NOT_FOUND ์๋ฌ๋ฅผ ๋ฐํํด์ผ ํ๋ค", async () => {
- vi.mocked(prisma.brand.findUnique).mockResolvedValue(null);
-
- const result = await getBrandByIdService("non-existent");
-
- expect(result.success).toBe(false);
- if (!result.success) {
- expect(result.error).toBe("NOT_FOUND");
- }
- });
- });
-
- describe("getBrandByNameService", () => {
- it("์ด๋ฆ์ผ๋ก ๋ธ๋๋๋ฅผ ์กฐํํด์ผ ํ๋ค", async () => {
- const mockBrand = {
- id: "brand-1",
- nameEn: "Test Brand",
- nameKo: "ํ
์คํธ ๋ธ๋๋",
- description: "Test Description",
- imageUrl: "test.jpg",
- brandUrl: "https://test.com",
- mapLocation: null,
- };
-
- vi.mocked(prisma.brand.findUnique).mockResolvedValue(mockBrand as never);
-
- const result = await getBrandByNameService("Test Brand");
-
- expect(result.success).toBe(true);
- if (result.success) {
- expect(result.data.nameEn).toBe("Test Brand");
- }
- });
-
- it("์กด์ฌํ์ง ์๋ ๋ธ๋๋๋ NOT_FOUND ์๋ฌ๋ฅผ ๋ฐํํด์ผ ํ๋ค", async () => {
- vi.mocked(prisma.brand.findUnique).mockResolvedValue(null);
-
- const result = await getBrandByNameService("Non Existent Brand");
-
- expect(result.success).toBe(false);
- if (!result.success) {
- expect(result.error).toBe("NOT_FOUND");
- }
- });
- });
-});
diff --git a/src/lib/hono/services/__tests__/point.service.test.ts b/src/lib/hono/services/__tests__/point.service.test.ts
deleted file mode 100644
index aad9de7c..00000000
--- a/src/lib/hono/services/__tests__/point.service.test.ts
+++ /dev/null
@@ -1,112 +0,0 @@
-import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
-import { validateTimestamp } from "../point.service";
-
-/**
- * validateTimestamp ํจ์ ํ
์คํธ
- *
- * ํ
์คํธ ์ ๋ต:
- * - ์ค๋ณต ๋ก์ง์ ๋ฐฐ์ด๊ณผ forEach๋ก ํตํฉํ์ฌ ๊ฐ๊ฒฐ์ฑ ํ๋ณด
- * - ๊ธฐ๋ณธ ์๋์ฐ(5๋ถ)์ ์ปค์คํ
์๋์ฐ๋ฅผ ๋ถ๋ฆฌํ์ฌ ๊ฒ์ฆ
- * - ๊ฒฝ๊ณ๊ฐ(ยฑ1ms)์ ๋ช
์์ ์ผ๋ก ํ
์คํธ
- * - vi.setSystemTime()์ ์ฌ์ฉํ์ฌ ์๊ฐ ๊ณ ์
- *
- * ์ฃผ์ ์๋๋ฆฌ์ค:
- * 1. ๊ธฐ๋ณธ ์๋์ฐ (5๋ถ ์ด๋ด ์ ํจ์ฑ ๊ฒ์ฆ)
- * 2. ์ปค์คํ
์๋์ฐ (๋ค์ํ ์๊ฐ ๋ฒ์ ๊ฒ์ฆ)
- * 3. ๊ฒฝ๊ณ๊ฐ ํ
์คํธ (์ ํํ ๊ฒฝ๊ณ์์์ ๋์ ๊ฒ์ฆ)
- */
-
-describe("Point Service - validateTimestamp", () => {
- const FIVE_MINUTES = 5 * 60 * 1000;
- const FIXED_TIME = new Date("2025-11-21T00:00:00.000Z");
-
- beforeEach(() => {
- // ์์คํ
์๊ฐ์ ๊ณ ์
- vi.setSystemTime(FIXED_TIME);
- });
-
- afterEach(() => {
- // ์์คํ
์๊ฐ ๋ณต์
- vi.useRealTimers();
- });
-
- describe("๊ธฐ๋ณธ ์๋์ฐ (5๋ถ)", () => {
- it("5๋ถ ๋ฏธ๋ง ์๊ฐ์ ์ ํจํด์ผ ํ๋ค", () => {
- const now = FIXED_TIME.getTime();
- const testCases = [
- new Date(now), // ํ์ฌ
- new Date(now - 4 * 60 * 1000), // 4๋ถ ์
- new Date(now + 4 * 60 * 1000), // 4๋ถ ํ
- new Date(now - (FIVE_MINUTES - 1)), // 5๋ถ - 1ms ์
- new Date(now + (FIVE_MINUTES - 1)), // 5๋ถ - 1ms ํ
- ];
-
- testCases.forEach((timestamp) => {
- expect(validateTimestamp(timestamp)).toBe(true);
- });
- });
-
- it("5๋ถ ์ด์ ์๊ฐ์ ์ ํจํ์ง ์์์ผ ํ๋ค", () => {
- const now = FIXED_TIME.getTime();
- const testCases = [
- new Date(now - FIVE_MINUTES), // ์ ํํ 5๋ถ ์
- new Date(now + FIVE_MINUTES), // ์ ํํ 5๋ถ ํ
- new Date(now - 6 * 60 * 1000), // 6๋ถ ์
- new Date(now + 6 * 60 * 1000), // 6๋ถ ํ
- new Date(now - 60 * 60 * 1000), // 1์๊ฐ ์
- new Date(now - 24 * 60 * 60 * 1000), // 1์ผ ์
- ];
-
- testCases.forEach((timestamp) => {
- expect(validateTimestamp(timestamp)).toBe(false);
- });
- });
- });
-
- describe("์ปค์คํ
์๋์ฐ", () => {
- it("์ปค์คํ
์๋์ฐ ์ด๋ด๋ ์ ํจํด์ผ ํ๋ค", () => {
- const now = FIXED_TIME.getTime();
- const testCases = [
- {
- timestamp: new Date(now - 9 * 60 * 1000),
- window: 10 * 60 * 1000,
- }, // 10๋ถ ์๋์ฐ์์ 9๋ถ ์
- { timestamp: new Date(now - 20 * 1000), window: 30 * 1000 }, // 30์ด ์๋์ฐ์์ 20์ด ์
- ];
-
- testCases.forEach(({ timestamp, window }) => {
- expect(validateTimestamp(timestamp, window)).toBe(true);
- });
- });
-
- it("์ปค์คํ
์๋์ฐ ์ด๊ณผ๋ ์ ํจํ์ง ์์์ผ ํ๋ค", () => {
- const now = FIXED_TIME.getTime();
- const testCases = [
- {
- timestamp: new Date(now - 11 * 60 * 1000),
- window: 10 * 60 * 1000,
- }, // 10๋ถ ์๋์ฐ์์ 11๋ถ ์
- {
- timestamp: new Date(now - 2 * 60 * 1000),
- window: 1 * 60 * 1000,
- }, // 1๋ถ ์๋์ฐ์์ 2๋ถ ์
- ];
-
- testCases.forEach(({ timestamp, window }) => {
- expect(validateTimestamp(timestamp, window)).toBe(false);
- });
- });
- });
-
- describe("๊ฒฝ๊ณ๊ฐ", () => {
- it("์ ํํ ๊ฒฝ๊ณ๊ฐ์์ ยฑ1ms ๊ฒ์ฆ", () => {
- const now = FIXED_TIME.getTime();
- const boundary = FIVE_MINUTES;
-
- expect(validateTimestamp(new Date(now - boundary))).toBe(false); // 5๋ถ ์ ํํ (์ ํจํ์ง ์์)
- expect(validateTimestamp(new Date(now + boundary))).toBe(false); // 5๋ถ ์ ํํ (๋ฏธ๋, ์ ํจํ์ง ์์)
- expect(validateTimestamp(new Date(now - (boundary + 1)))).toBe(false); // 5๋ถ + 1ms (์ ํจํ์ง ์์)
- expect(validateTimestamp(new Date(now - (boundary - 1)))).toBe(true); // 5๋ถ - 1ms (์ ํจํจ, 4๋ถ 59์ด 999ms)
- });
- });
-});
diff --git a/src/lib/hono/services/brand.service.ts b/src/lib/hono/services/brand.service.ts
deleted file mode 100644
index 908b791d..00000000
--- a/src/lib/hono/services/brand.service.ts
+++ /dev/null
@@ -1,115 +0,0 @@
-import { prisma } from "@/lib/prisma";
-import {
- serviceInternalError,
- serviceNotFound,
- ServiceResult,
- serviceSuccess,
-} from "../utils/service.utils";
-import {
- ApiBrandDetailResponse,
- ApiBrandSimpleResponse,
-} from "../schemas/brand.schema";
-import { brandDetailSelect, parseMapLocation } from "../utils/prisma.utils";
-
-/**
- * ๋ชจ๋ ๋ธ๋๋ ๋ชฉ๋ก ์กฐํ
- * - ๋ธ๋๋ ํํฐ๋ง, ๊ฒ์ ๋ฑ์ ์ฌ์ฉ
- * @returns ๋ชจ๋ ๋ธ๋๋์ ๊ฐ๋จ ์๋ต ๋ชฉ๋ก์ ๋ด์ ServiceResult
- */
-export async function getAllBrandsService(): Promise<
- ServiceResult
-> {
- try {
- const brands = await prisma.brand.findMany({
- select: {
- id: true,
- nameEn: true,
- nameKo: true,
- },
- orderBy: {
- nameKo: "asc",
- },
- });
-
- const transformed = brands.map((brand) => ({
- id: brand.id,
- nameEn: brand.nameEn,
- nameKo: brand.nameKo ?? brand.nameEn,
- }));
-
- return serviceSuccess(transformed);
- } catch (error) {
- return serviceInternalError(error);
- }
-}
-
-/**
- * ๋ธ๋๋ ID๋ก ์์ธ ์กฐํ
- * @param brandId ์กฐํํ ๋ธ๋๋ ID
- * @returns ๋ธ๋๋ ์์ธ ์ ๋ณด๋ฅผ ๋ด์ ServiceResult
- */
-export async function getBrandByIdService(
- brandId: string
-): Promise> {
- try {
- const brand = await prisma.brand.findUnique({
- where: { id: brandId },
- select: brandDetailSelect,
- });
-
- if (!brand) {
- return serviceNotFound("๋ธ๋๋๋ฅผ ์ฐพ์ ์ ์์ต๋๋ค.");
- }
-
- // Prisma ํ์
์ API ํ์
์ผ๋ก ๋ณํ
- const transformed: ApiBrandDetailResponse = {
- id: brand.id,
- nameEn: brand.nameEn,
- nameKo: brand.nameKo ?? brand.nameEn,
- description: brand.description,
- imageUrl: brand.imageUrl,
- brandUrl: brand.brandUrl,
- mapLocation: parseMapLocation(brand.mapLocation),
- };
-
- return serviceSuccess(transformed);
- } catch (error) {
- return serviceInternalError(error);
- }
-}
-
-/**
- * ๋ธ๋๋ ํ๊ธ ์ด๋ฆ์ผ๋ก ์์ธ ์กฐํ
- * - brand/[name] ํ์ด์ง์์ ์ฌ์ฉ
- * @param nameKo ์กฐํํ ๋ธ๋๋์ ํ๊ธ ์ด๋ฆ
- * @returns ๋ธ๋๋ ์์ธ ์ ๋ณด๋ฅผ ๋ด์ ServiceResult
- */
-export async function getBrandByNameService(
- nameKo: string
-): Promise> {
- try {
- const brand = await prisma.brand.findUnique({
- where: { nameKo },
- select: brandDetailSelect,
- });
-
- if (!brand) {
- return serviceNotFound("๋ธ๋๋๋ฅผ ์ฐพ์ ์ ์์ต๋๋ค.");
- }
-
- // Prisma ํ์
์ API ํ์
์ผ๋ก ๋ณํ
- const transformed: ApiBrandDetailResponse = {
- id: brand.id,
- nameEn: brand.nameEn,
- nameKo: brand.nameKo ?? brand.nameEn,
- description: brand.description,
- imageUrl: brand.imageUrl,
- brandUrl: brand.brandUrl,
- mapLocation: parseMapLocation(brand.mapLocation),
- };
-
- return serviceSuccess(transformed);
- } catch (error) {
- return serviceInternalError(error);
- }
-}
diff --git a/src/lib/hono/services/community.service.ts b/src/lib/hono/services/community.service.ts
deleted file mode 100644
index 86898b07..00000000
--- a/src/lib/hono/services/community.service.ts
+++ /dev/null
@@ -1,505 +0,0 @@
-import { PostCategory, Prisma, PointActivityType } from "@prisma/client";
-import { prisma } from "@/lib/prisma";
-import type {
- ApiPostDetailCategoryPostResponse,
- ApiPostDetailResponse,
- ApiPostStatusResponse,
- CreatePostInput,
- GetPostsQuery,
- UpdatePostInput,
-} from "../schemas/community.schema";
-import {
- createCursorPaginationResult,
- PaginationResult,
-} from "../utils/pagination.utils";
-import {
- checkResourceExists,
- serviceBadRequest,
- serviceForbidden,
- serviceInternalError,
- serviceNotFound,
- ServiceResult,
- serviceSuccess,
- validateUuid,
-} from "../utils/service.utils";
-import {
- postIncludeArgs,
- postDetailIncludeArgs,
- BasePost,
-} from "../utils/prisma.utils";
-import { earnPointsService } from "./point.service";
-import { sanitizeHtml } from "../utils/sanitize.utils";
-
-const postWithAuthorArgs = { include: postIncludeArgs };
-
-/**
- * ๊ฒ์๊ธ ๋ชฉ๋ก์ ํ์ด์ง๋ค์ด์
ํ์ฌ ์กฐํํฉ๋๋ค.
- * @param params - ๊ฒ์๊ธ ์กฐํ ์ฟผ๋ฆฌ (์นดํ
๊ณ ๋ฆฌ, ์ ๋ ฌ, ๊ฒ์์ด, ์ปค์, ์ ํ)
- */
-export async function getPaginatedPostListService(
- params: GetPostsQuery,
-): Promise>> {
- try {
- const { category, sortBy, q, cursor, limit } = params;
- const fetchLimit = limit + 1;
-
- const where: Prisma.PostWhereInput = { published: true };
- if (category) where.category = category;
- if (q) {
- where.OR = [
- { title: { contains: q, mode: "insensitive" } },
- { contentText: { contains: q, mode: "insensitive" } },
- ];
- }
-
- const orderBy:
- | Prisma.PostOrderByWithRelationInput[]
- | Prisma.PostOrderByWithRelationInput =
- sortBy === "popular"
- ? [{ likeCount: "desc" }, { createdAt: "desc" }]
- : { createdAt: "desc" };
-
- const [posts, totalCount] = await Promise.all([
- prisma.post.findMany({
- where,
- orderBy,
- take: fetchLimit,
- cursor: cursor ? { id: cursor } : undefined,
- ...postWithAuthorArgs,
- }),
- prisma.post.count({ where }),
- ]);
-
- const paginatedResult = createCursorPaginationResult(
- posts,
- totalCount,
- limit,
- );
- return serviceSuccess(paginatedResult);
- } catch (error) {
- return serviceInternalError(error);
- }
-}
-
-/**
- * ๊ฒ์๊ธ ID๋ก ์์ธ ์ ๋ณด๋ฅผ ์กฐํํฉ๋๋ค.
- * @param id - ๊ฒ์๊ธ ID
- * @param userId - ์ธ์ฆ๋ ์ฌ์ฉ์ ID (์์ฑ์ ์ฌ๋ถ ํ์ธ์ฉ)
- */
-export async function getPostByIdService(
- id: string,
- userId?: string | null,
-): Promise> {
- try {
- const uuidValidation = validateUuid(id, "๊ฒ์๊ธ");
- if (!uuidValidation.success) return uuidValidation;
-
- const post = await prisma.$transaction(async (tx) => {
- await tx.post.update({
- where: { id },
- data: { viewCount: { increment: 1 } },
- });
- return tx.post.findUnique({
- where: { id },
- include: postDetailIncludeArgs,
- });
- });
-
- if (!post) {
- return serviceNotFound("๊ฒ์๊ธ์ ์ฐพ์ ์ ์์ต๋๋ค.");
- }
- if (!post.published) {
- return serviceForbidden("์ด๋ฏธ ์ญ์ ๋ ๊ฒ์๊ธ์
๋๋ค.");
- }
- const { perfumeMappings, ...restOfPost } = post;
- const isAuthor = post.userId === userId;
-
- const result = {
- ...restOfPost,
- content: sanitizeHtml(post.content),
- isAuthor,
- perfumes: perfumeMappings.map((mapping) => mapping.perfume),
- createdAt: post.createdAt.toISOString(),
- updatedAt: post.updatedAt?.toISOString() || null,
- };
-
- return serviceSuccess(result);
- } catch (error) {
- return serviceInternalError(error);
- }
-}
-
-/**
- * ๊ฒ์๊ธ์ ์กฐํ์, ์ข์์, ๋๊ธ ์ ๋ฑ ์ํ ์ ๋ณด๋ฅผ ์กฐํํฉ๋๋ค.
- * @param postId - ๊ฒ์๊ธ ID
- * @param userId - ์ธ์ฆ๋ ์ฌ์ฉ์ ID (์ข์์/๋ถ๋งํฌ ์ฌ๋ถ ํ์ธ์ฉ)
- */
-export async function getPostStatusByIdService(
- postId: string,
- userId?: string | null,
-): Promise> {
- try {
- const uuidValidation = validateUuid(postId, "๊ฒ์๊ธ");
- if (!uuidValidation.success) return uuidValidation;
- const [postCounts, like, bookmark] = await Promise.all([
- prisma.post.findUnique({
- where: { id: postId },
- select: { viewCount: true, likeCount: true, commentCount: true },
- }),
-
- userId
- ? prisma.postLike.findUnique({
- where: { post_likes_user_id_post_id_key: { postId, userId } },
- })
- : null,
- userId
- ? prisma.postBookmark.findUnique({
- where: { post_bookmarks_user_id_post_id_key: { postId, userId } },
- })
- : null,
- ]);
-
- if (!postCounts) {
- return serviceNotFound("๊ฒ์๊ธ ์ํ ์ ๋ณด๋ฅผ ์ฐพ์ ์ ์์ต๋๋ค.");
- }
-
- const result: ApiPostStatusResponse = {
- ...postCounts,
- isLiked: !!like,
- isBookmarked: !!bookmark,
- };
-
- return serviceSuccess(result);
- } catch (error) {
- return serviceInternalError(error);
- }
-}
-
-/**
- * ๊ฒ์๊ธ๊ณผ ๊ฐ์ ์นดํ
๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ฒ์๊ธ ๋ชฉ๋ก์ ์กฐํํฉ๋๋ค.
- * @param postId - ํ์ฌ ๊ฒ์๊ธ ID
- */
-export async function getPostDetailCategoryPostsService(
- postId: string,
-): Promise> {
- try {
- const uuidValidation = validateUuid(postId, "๊ฒ์๊ธ");
- if (!uuidValidation.success) return uuidValidation;
- const currentPost = await prisma.post.findUnique({
- where: { id: postId },
- select: { id: true, createdAt: true, category: true },
- });
- if (!currentPost) {
- return serviceNotFound("๊ฒ์๊ธ์ ์ฐพ์ ์ ์์ต๋๋ค.");
- }
-
- const { category, createdAt } = currentPost;
- const TOTAL_LIMIT = 15;
- const PREV_LIMIT_GOAL = 2;
-
- const prevPosts = await prisma.post.findMany({
- where: {
- category,
- published: true,
- createdAt: { gt: createdAt },
- },
- orderBy: { createdAt: "asc" },
- take: PREV_LIMIT_GOAL,
- include: { author: { select: { nickname: true } } },
- });
- const missingCount = PREV_LIMIT_GOAL - prevPosts.length;
- const nextLimitBase = TOTAL_LIMIT - PREV_LIMIT_GOAL;
- const nextPosts = await prisma.post.findMany({
- where: {
- category,
- published: true,
- createdAt: { lte: createdAt },
- },
- orderBy: { createdAt: "desc" },
- take: nextLimitBase + (missingCount > 0 ? missingCount : 0),
- include: { author: { select: { nickname: true } } },
- });
- const combinedPosts = [...prevPosts.reverse(), ...nextPosts];
-
- const result: ApiPostDetailCategoryPostResponse[] = combinedPosts.map(
- (post) => ({
- id: post.id,
- title: post.title,
- commentCount: post.commentCount,
- createdAt: post.createdAt,
- author: {
- id: post.userId,
- nickname: post.author.nickname,
- },
- }),
- );
- return serviceSuccess(result);
- } catch (error) {
- return serviceInternalError(error);
- }
-}
-
-/**
- * ์ ๊ฒ์๊ธ์ ์์ฑํฉ๋๋ค.
- * @param payload - ๊ฒ์๊ธ ์์ฑ ๋ฐ์ดํฐ (์์ฑ์ ID ํฌํจ)
- */
-export async function createPostService(
- payload: CreatePostInput & { authorId: string },
-): Promise> {
- try {
- const { authorId, perfumeIds, ...rest } = payload;
- const userCheck = await checkResourceExists("user", authorId, "์ฌ์ฉ์");
- if (!userCheck.success) return userCheck;
-
- const newPost = await prisma.post.create({
- data: {
- ...rest,
- userId: authorId,
- perfumeMappings: perfumeIds
- ? { create: perfumeIds.map((perfumeId) => ({ perfumeId })) }
- : undefined,
- },
- include: postIncludeArgs,
- });
-
- // ํฌ์ธํธ ์ ๋ฆฝ (๋น๋๊ธฐ, ์คํจํด๋ ๊ฒ์๊ธ ์์ฑ์ ์ฑ๊ณต)
- earnPointsService(
- authorId,
- PointActivityType.CREATE_POST,
- newPost.id,
- ).catch((error) => {
- console.error("[Point] Failed to earn points for post creation:", error);
- });
-
- return serviceSuccess(newPost);
- } catch (error) {
- return serviceInternalError(error);
- }
-}
-
-/**
- * ๊ฒ์๊ธ์ ์์ ํฉ๋๋ค.
- * @param postId - ์์ ํ ๊ฒ์๊ธ ID
- * @param authorId - ์์ฑ์ ID (๊ถํ ํ์ธ์ฉ)
- * @param updateData - ์์ ํ ๋ฐ์ดํฐ
- */
-export async function updatePostService(
- postId: string,
- authorId: string,
- updateData: UpdatePostInput,
-): Promise> {
- try {
- const { perfumeIds, ...postUpdateData } = updateData;
- const [uuidValidation, userCheck] = await Promise.all([
- validateUuid(postId, "๊ฒ์๊ธ"),
- checkResourceExists("user", authorId, "์ฌ์ฉ์"),
- ]);
- if (!uuidValidation.success) return uuidValidation;
- if (!userCheck.success) return userCheck;
-
- const post = await prisma.post.findUnique({
- where: { id: postId },
- select: { published: true, author: { select: { id: true } } },
- });
- if (!post) {
- return serviceNotFound("๊ฒ์๊ธ์ ์ฐพ์ ์ ์์ต๋๋ค.");
- }
- if (post.author.id !== authorId) {
- return serviceForbidden("๊ฒ์๊ธ์ ์์ ํ ๊ถํ์ด ์์ต๋๋ค.");
- }
- if (!post.published) {
- return serviceBadRequest("์ด๋ฏธ ์ญ์ ๋ ๊ฒ์๊ธ์ ์์ ํ ์ ์์ต๋๋ค.");
- }
-
- const updatedPost = await prisma.$transaction(async (tx) => {
- if (perfumeIds !== undefined) {
- await tx.postPerfumeMapping.deleteMany({ where: { postId } });
- }
-
- const post = await tx.post.update({
- where: { id: postId, author: { id: authorId } },
- data: {
- ...postUpdateData,
- perfumeMappings:
- perfumeIds && perfumeIds.length > 0
- ? {
- create: perfumeIds.map((perfumeId) => ({
- perfume: { connect: { id: perfumeId } },
- })),
- }
- : undefined,
- },
- include: postIncludeArgs,
- });
- return post;
- });
-
- return serviceSuccess(updatedPost);
- } catch (error) {
- return serviceInternalError(error);
- }
-}
-
-/**
- * ๊ฒ์๊ธ์ ์ญ์ ํฉ๋๋ค (์ํํธ ์ญ์ ).
- * @param postId - ์ญ์ ํ ๊ฒ์๊ธ ID
- * @param authorId - ์์ฑ์ ID (๊ถํ ํ์ธ์ฉ)
- */
-export async function deletePostService(
- postId: string,
- authorId: string,
-): Promise> {
- try {
- const [uuidValidation, userCheck] = await Promise.all([
- validateUuid(postId, "๊ฒ์๊ธ"),
- checkResourceExists("user", authorId, "์ฌ์ฉ์"),
- ]);
- if (!uuidValidation.success) return uuidValidation;
- if (!userCheck.success) return userCheck;
-
- const post = await prisma.post.findUnique({
- where: { id: postId },
- });
- if (!post) return serviceNotFound("๊ฒ์๊ธ์ ์ฐพ์ ์ ์์ต๋๋ค.");
- if (post.userId !== authorId)
- return serviceForbidden("๊ฒ์๊ธ ์ญ์ ๊ถํ์ด ์์ต๋๋ค.");
- if (!post.published) return serviceBadRequest("์ด๋ฏธ ์ญ์ ๋ ๊ฒ์๊ธ์
๋๋ค.");
-
- await prisma.post.update({
- where: { id: postId },
- data: { published: false },
- });
-
- return serviceSuccess({ category: post.category });
- } catch (error) {
- return serviceInternalError(error);
- }
-}
-
-/**
- * ๊ฒ์๊ธ ์ข์์๋ฅผ ํ ๊ธํฉ๋๋ค.
- * @param postId - ๊ฒ์๊ธ ID
- * @param userId - ์ฌ์ฉ์ ID
- */
-export async function togglePostLikeService(
- postId: string,
- userId: string,
-): Promise> {
- try {
- const [uuidValidation, userCheck] = await Promise.all([
- validateUuid(postId, "๊ฒ์๊ธ"),
- checkResourceExists("user", userId, "์ฌ์ฉ์"),
- ]);
- if (!uuidValidation.success) return uuidValidation;
- if (!userCheck.success) return userCheck;
-
- const post = await prisma.post.findUnique({
- where: { id: postId },
- select: { published: true, userId: true },
- });
-
- if (!post) {
- return serviceNotFound("๊ฒ์๊ธ์ ์ฐพ์ ์ ์์ต๋๋ค.");
- }
- if (!post.published) {
- return serviceForbidden("์ด๋ฏธ ์ญ์ ๋ ๊ฒ์๊ธ์
๋๋ค.");
- }
-
- const like = await prisma.postLike.findUnique({
- where: { post_likes_user_id_post_id_key: { postId, userId } },
- });
-
- if (like) {
- const [, updatedPost] = await prisma.$transaction([
- prisma.postLike.delete({ where: { id: like.id } }),
- prisma.post.update({
- where: { id: postId },
- data: { likeCount: { decrement: 1 } },
- select: { likeCount: true },
- }),
- ]);
- return serviceSuccess({ liked: false, likeCount: updatedPost.likeCount });
- } else {
- const [createdLike, updatedPost] = await prisma.$transaction([
- prisma.postLike.create({ data: { postId, userId } }),
- prisma.post.update({
- where: { id: postId },
- data: { likeCount: { increment: 1 } },
- select: { likeCount: true },
- }),
- ]);
-
- // ํฌ์ธํธ ์ ๋ฆฝ: ๊ธ ์์ฑ์์๊ฒ ์ง๊ธ (๋น๋๊ธฐ, ์คํจํด๋ ์ข์์๋ ์ฑ๊ณต)
- earnPointsService(
- post.userId, // ์ข์์๋ฅผ ๋๋ฅธ ์ฌ๋(userId)์ด ์๋, ๊ธ ์์ฑ์(post.userId)์๊ฒ ํฌ์ธํธ ์ง๊ธ
- PointActivityType.LIKE_POST,
- createdLike.id,
- ).catch((error) => {
- console.error("[Point] Failed to earn points for post like:", error);
- });
-
- return serviceSuccess({ liked: true, likeCount: updatedPost.likeCount });
- }
- } catch (error) {
- return serviceInternalError(error);
- }
-}
-
-/**
- * ๊ฒ์๊ธ ๋ถ๋งํฌ๋ฅผ ํ ๊ธํฉ๋๋ค.
- * @param postId - ๊ฒ์๊ธ ID
- * @param userId - ์ฌ์ฉ์ ID
- */
-export async function togglePostBookmarkService(
- postId: string,
- userId: string,
-): Promise> {
- try {
- const [uuidValidation, userCheck] = await Promise.all([
- validateUuid(postId, "๊ฒ์๊ธ"),
- checkResourceExists("user", userId, "์ฌ์ฉ์"),
- ]);
- if (!uuidValidation.success) return uuidValidation;
- if (!userCheck.success) return userCheck;
-
- const post = await prisma.post.findUnique({
- where: { id: postId },
- select: { published: true },
- });
-
- if (!post) {
- return serviceNotFound("๊ฒ์๊ธ์ ์ฐพ์ ์ ์์ต๋๋ค.");
- }
- if (!post.published) {
- return serviceForbidden("์ด๋ฏธ ์ญ์ ๋ ๊ฒ์๊ธ์
๋๋ค.");
- }
-
- const bookmark = await prisma.postBookmark.findUnique({
- where: { post_bookmarks_user_id_post_id_key: { postId, userId } },
- });
-
- if (bookmark) {
- await prisma.postBookmark.delete({ where: { id: bookmark.id } });
- return serviceSuccess({ bookmarked: false });
- } else {
- await prisma.postBookmark.create({ data: { postId, userId } });
- return serviceSuccess({ bookmarked: true });
- }
- } catch (error) {
- return serviceInternalError(error);
- }
-}
-
-// export async function getBookmarkedPostService(userId: string) {
-// try {
-// const bookmarkedPosts = await prisma.postBookmark.findMany({
-// where: { userId },
-// include: {
-// post: true,
-// },
-// });
-// return serviceSuccess(bookmarkedPosts);
-// } catch (error) {
-// return serviceInternalError(error);
-// }
-// }
diff --git a/src/lib/hono/services/me.service.ts b/src/lib/hono/services/me.service.ts
deleted file mode 100644
index e2f273f5..00000000
--- a/src/lib/hono/services/me.service.ts
+++ /dev/null
@@ -1,554 +0,0 @@
-import { ImageFormat } from "@prisma/client";
-import { prisma } from "@/lib/prisma";
-import {
- ServiceResult,
- serviceSuccess,
- serviceInternalError,
- serviceNotFound,
- serviceForbidden,
-} from "../utils/service.utils";
-import {
- ApiMyProfileResponse,
- ApiUpdateMyProfileRequest,
- ApiUpdateMyProfileRequestSchema,
-} from "../schemas/me.schema";
-import {
- COLLECTION_BUCKET_NAME,
- PROFILE_BUCKET_NAME,
-} from "@/lib/constants/buckets";
-import { PaginatedResponse } from "../schemas/common.schema";
-import { deleteImageByUrl } from "./file.service";
-import {
- BasePost,
- BasePerfume,
- FullReview,
- MyCollection,
- MyComment,
- perfumeBaseInclude,
- postIncludeArgs,
- myCollectionInclude,
- myCommentInclude,
- reviewIncludeArgs,
-} from "../utils/prisma.utils";
-import { calculateLevel, getPointsForNextLevel } from "../../utils/level.utils";
-
-const UUID_REGEX =
- /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;
-
-/**
- * ์ธ์ฆ๋ ์ฌ์ฉ์๊ฐ ๋ถ๋งํฌํ ๊ฒ์๊ธ ๋ชฉ๋ก์ ์กฐํํฉ๋๋ค.
- * @param userId - ์ธ์ฆ๋ ์ฌ์ฉ์์ ID
- */
-export async function getMyBookmarkedPostsService(
- userId: string
-): Promise> {
- try {
- const bookmarks = await prisma.postBookmark.findMany({
- where: { userId },
- include: { post: { include: postIncludeArgs } },
- orderBy: { createdAt: "desc" },
- });
-
- const posts = bookmarks.map((b) => b.post);
- return serviceSuccess(posts);
- } catch (error) {
- return serviceInternalError(error);
- }
-}
-
-/**
- * ์ธ์ฆ๋ ์ฌ์ฉ์๊ฐ ์ต๊ทผ ๋ณธ ํฅ์ ๋ชฉ๋ก์ ์กฐํํฉ๋๋ค.
- * @param userId - ์ธ์ฆ๋ ์ฌ์ฉ์์ ID
- */
-export async function getRecentPerfumesService(userId: string): Promise<
- ServiceResult<{
- items: Array<{ id: string; viewedAt: Date; perfume: BasePerfume }>;
- }>
-> {
- try {
- const views = await prisma.recentPerfumeView.findMany({
- where: { userId },
- orderBy: { viewedAt: "desc" },
- include: { perfume: { include: perfumeBaseInclude } },
- });
-
- const items = views.map((v) => ({
- id: v.perfumeId,
- viewedAt: v.viewedAt,
- perfume: v.perfume as unknown as BasePerfume,
- }));
-
- return serviceSuccess({ items });
- } catch (error) {
- return serviceInternalError(error);
- }
-}
-
-/**
- * ์ธ์ฆ๋ ์ฌ์ฉ์๊ฐ ์ต๊ทผ ๋ณธ ๊ฒ์๊ธ ๋ชฉ๋ก์ ์กฐํํฉ๋๋ค.
- * @param userId - ์ธ์ฆ๋ ์ฌ์ฉ์์ ID
- */
-export async function getRecentPostsService(userId: string): Promise<
- ServiceResult<{
- items: Array<{ id: string; viewedAt: Date; post: BasePost }>;
- }>
-> {
- try {
- const views = await prisma.recentPostView.findMany({
- where: { userId },
- orderBy: { viewedAt: "desc" },
- include: { post: { include: postIncludeArgs } },
- });
-
- const items = views.map((v) => ({
- id: v.postId,
- viewedAt: v.viewedAt,
- post: v.post as unknown as BasePost,
- }));
-
- return serviceSuccess({ items });
- } catch (error) {
- return serviceInternalError(error);
- }
-}
-
-// ์ต๊ทผ ๋ณธ ํญ๋ชฉ ๋๊ธฐํ ๋ก์ง
-const MAX_RECENT_ITEMS = 50;
-
-export async function syncRecentPerfumesService(payload: {
- userId: string;
- perfumeIds: string[];
-}): Promise> {
- const { userId, perfumeIds } = payload;
- try {
- const now = new Date();
-
- for (const perfumeId of perfumeIds) {
- await prisma.recentPerfumeView.upsert({
- where: {
- recent_perfume_user_id_perfume_id_key: { userId, perfumeId },
- },
- create: { userId, perfumeId, viewedAt: now },
- update: { viewedAt: now },
- });
- }
-
- const toDelete = await prisma.recentPerfumeView.findMany({
- where: { userId },
- orderBy: { viewedAt: "desc" },
- skip: MAX_RECENT_ITEMS,
- select: { id: true },
- });
- if (toDelete.length > 0) {
- await prisma.recentPerfumeView.deleteMany({
- where: { id: { in: toDelete.map((r: { id: string }) => r.id) } },
- });
- }
-
- return serviceSuccess({ syncedCount: perfumeIds.length });
- } catch (error) {
- return serviceInternalError(error);
- }
-}
-
-export async function syncRecentPostsService(payload: {
- userId: string;
- postIds: string[];
-}): Promise> {
- const { userId, postIds } = payload;
- try {
- const now = new Date();
-
- for (const postId of postIds) {
- await prisma.recentPostView.upsert({
- where: {
- recent_post_user_id_post_id_key: { userId, postId },
- },
- create: { userId, postId, viewedAt: now },
- update: { viewedAt: now },
- });
- }
-
- const toDelete = await prisma.recentPostView.findMany({
- where: { userId },
- orderBy: { viewedAt: "desc" },
- skip: MAX_RECENT_ITEMS,
- select: { id: true },
- });
- if (toDelete.length > 0) {
- await prisma.recentPostView.deleteMany({
- where: { id: { in: toDelete.map((r: { id: string }) => r.id) } },
- });
- }
-
- return serviceSuccess({ syncedCount: postIds.length });
- } catch (error) {
- return serviceInternalError(error);
- }
-}
-
-/**
- * ์ธ์ฆ๋ ์ฌ์ฉ์๊ฐ ๋ถ๋งํฌํ ํฅ์ ๋ชฉ๋ก์ ์กฐํํฉ๋๋ค.
- * @param userId - ์ธ์ฆ๋ ์ฌ์ฉ์์ ID
- */
-export async function getMyBookmarkedPerfumesService(
- userId: string
-): Promise> {
- try {
- const bookmarks = await prisma.perfumeBookmark.findMany({
- where: { userId },
- include: { perfume: { include: perfumeBaseInclude } },
- orderBy: { createdAt: "desc" },
- });
-
- const perfumes = bookmarks.map((b) => b.perfume);
- return serviceSuccess(perfumes);
- } catch (error) {
- return serviceInternalError(error);
- }
-}
-
-/**
- * ์ธ์ฆ๋ ์ฌ์ฉ์๊ฐ ํฅ์ ์ปฌ๋ ์
์ ๋ฑ๋กํฉ๋๋ค.
- * @param payload - ํฅ์ ์ปฌ๋ ์
๋ฑ๋ก ์ ๋ณด
- * @param payload.userId - ์ธ์ฆ๋ ์ฌ์ฉ์์ ID
- * @param payload.perfumeId - ํฅ์ ID
- * @param payload.imageInfo - ์ด๋ฏธ์ง ์ ๋ณด
- * @param payload.comment - ๋๊ธ
- */
-
-interface PostCollectionPayload {
- userId: string;
- perfumeId: string;
- comment?: string;
- imageInfo: {
- imageUrl: string;
- width: number;
- height: number;
- format: ImageFormat;
- };
-}
-
-export async function postPhotoCollectionService(
- payload: PostCollectionPayload
-): Promise> {
- // ์
๋ ฅ๊ฐ ๊ฒ์ฆ
- if (!payload.perfumeId || !UUID_REGEX.test(payload.perfumeId)) {
- return serviceInternalError(new Error("ํฅ์ ID๊ฐ ์ฌ๋ฐ๋ฅด์ง ์์ต๋๋ค."));
- }
- if (!payload.imageInfo.imageUrl) {
- return serviceInternalError(new Error("์ด๋ฏธ์ง URL์ด ํ์ํฉ๋๋ค."));
- }
-
- try {
- // 0. ์ค๋ณต ์ฒดํฌ
- const existingCollection = await prisma.userCollection.findUnique({
- where: {
- user_collections_user_id_perfume_id_key: {
- userId: payload.userId,
- perfumeId: payload.perfumeId,
- },
- },
- include: { image: true },
- });
- if (existingCollection?.image) {
- await deleteImageByUrl(
- COLLECTION_BUCKET_NAME,
- existingCollection.image.imageUrl
- );
- }
-
- const newImageData = {
- imageUrl: payload.imageInfo.imageUrl,
- width: payload.imageInfo.width,
- height: payload.imageInfo.height,
- format: payload.imageInfo.format,
- };
-
- const resultCollection = await prisma.userCollection.upsert({
- where: {
- user_collections_user_id_perfume_id_key: {
- userId: payload.userId,
- perfumeId: payload.perfumeId,
- },
- },
- create: {
- userId: payload.userId,
- perfumeId: payload.perfumeId,
- comment: payload.comment || null,
- image: { create: newImageData },
- },
- update: {
- comment: payload.comment || null,
- image: {
- delete: existingCollection?.image ? true : false,
- create: newImageData,
- },
- },
- include: myCollectionInclude,
- });
-
- return serviceSuccess(resultCollection);
- } catch (error) {
- return serviceInternalError(error);
- }
-}
-
-export async function deletePhotoCollectionService(payload: {
- userId: string;
- collectionId: string;
-}): Promise> {
- const { userId, collectionId } = payload;
- try {
- const collection = await prisma.userCollection.findUnique({
- where: { id: collectionId },
- include: { image: true },
- });
-
- if (!collection) {
- return serviceNotFound("ํด๋น ์ปฌ๋ ์
์ ์ฐพ์ ์ ์์ต๋๋ค.");
- }
- if (collection.userId !== userId) {
- return serviceForbidden("์ปฌ๋ ์
์ ์ญ์ ํ ๊ถํ์ด ์์ต๋๋ค.");
- }
-
- if (collection.image) {
- await deleteImageByUrl(COLLECTION_BUCKET_NAME, collection.image.imageUrl);
- }
-
- await prisma.userCollection.delete({
- where: {
- id: collectionId,
- },
- });
-
- return serviceSuccess({ message: "์ปฌ๋ ์
์ ์ญ์ ํ์ต๋๋ค." });
- } catch (error) {
- return serviceInternalError(error);
- }
-}
-
-/**
- * ์ฌ์ฉ์๊ฐ ์์ฑํ ๋ฆฌ๋ทฐ ๋ชฉ๋ก์ ์กฐํํฉ๋๋ค.
- * @param userId - ์ฌ์ฉ์์ ID
- * @returns ์ฌ์ฉ์์ ํฅ์ ์ปฌ๋ ์
- */
-export async function getMyReviewsService(
- userId: string,
- options: { limit?: number; cursor?: string }
-): Promise>> {
- try {
- const { limit = 12, cursor } = options;
- const [reviews, totalCount] = await Promise.all([
- prisma.review.findMany({
- where: { authorId: userId },
- include: reviewIncludeArgs,
- orderBy: { createdAt: "desc" },
- take: limit + 1,
- ...(cursor && { cursor: { id: cursor }, skip: 1 }),
- }),
- prisma.review.count({ where: { authorId: userId } }),
- ]);
-
- const hasMore = reviews.length > limit;
- const data = hasMore ? reviews.slice(0, limit) : reviews;
- const nextCursor = hasMore ? data[data.length - 1].id : null;
-
- return serviceSuccess({ data, totalCount, nextCursor });
- } catch (error) {
- return serviceInternalError(error);
- }
-}
-/**
- * ์ฌ์ฉ์์ ์์ฑํ ๊ฒ์๊ธ ๋ชฉ๋ก์ ์กฐํํฉ๋๋ค.
- * @param userId - ์ฌ์ฉ์์ ID
- * @returns ์ฌ์ฉ์์ ์์ฑํ ๊ฒ์๊ธ ๋ชฉ๋ก
- */
-export async function getMyPostsService(
- userId: string,
- options: { limit?: number; cursor?: string }
-): Promise>> {
- try {
- const { limit = 12, cursor } = options;
- const [posts, totalCount] = await Promise.all([
- prisma.post.findMany({
- where: { userId },
- include: postIncludeArgs,
- orderBy: { createdAt: "desc" },
- take: limit + 1,
- ...(cursor && { cursor: { id: cursor }, skip: 1 }),
- }),
- prisma.post.count({ where: { userId } }),
- ]);
-
- const hasMore = posts.length > limit;
- const data = hasMore ? posts.slice(0, limit) : posts;
- const nextCursor = hasMore ? data[data.length - 1].id : null;
-
- return serviceSuccess({ data, totalCount, nextCursor });
- } catch (error) {
- return serviceInternalError(error);
- }
-}
-
-/**
- * ์ฌ์ฉ์์ ์์ฑํ ๋๊ธ ๋ชฉ๋ก์ ์กฐํํฉ๋๋ค.
- * @param userId - ์ฌ์ฉ์์ ID
- * @returns ์ฌ์ฉ์์ ์์ฑํ ๋๊ธ ๋ชฉ๋ก
- */
-export async function getMyCommentsService(
- userId: string,
- options: { limit?: number; cursor?: string }
-): Promise>> {
- try {
- const { limit = 12, cursor } = options;
- const [comments, totalCount] = await Promise.all([
- prisma.comment.findMany({
- where: { authorId: userId },
- include: myCommentInclude,
- orderBy: { createdAt: "desc" },
- take: limit + 1,
- ...(cursor && { cursor: { id: cursor }, skip: 1 }),
- }),
- prisma.comment.count({ where: { authorId: userId } }),
- ]);
-
- const hasMore = comments.length > limit;
- const data = hasMore ? comments.slice(0, limit) : comments;
- const nextCursor = hasMore ? data[data.length - 1].id : null;
-
- return serviceSuccess({ data, totalCount, nextCursor });
- } catch (error) {
- return serviceInternalError(error);
- }
-}
-
-/**
- * ์ฌ์ฉ์๊ฐ ์ข์์ํ ํฅ์ ๋ชฉ๋ก์ ์กฐํํฉ๋๋ค.
- * @param userId - ์ฌ์ฉ์์ ID
- * @returns ์ฌ์ฉ์์ ์ข์์ํ ํฅ์ ๋ชฉ๋ก
- */
-export async function getMyLikedPerfumesService(
- userId: string
-): Promise> {
- try {
- const likes = await prisma.perfumeLike.findMany({
- where: { userId },
- include: { perfume: { include: perfumeBaseInclude } },
- orderBy: { createdAt: "desc" },
- });
- return serviceSuccess(likes.map((like) => like.perfume));
- } catch (error) {
- return serviceInternalError(error);
- }
-}
-/**
- * ์ฌ์ฉ์๊ฐ ์ข์์ํ ๊ฒ์๊ธ ๋ชฉ๋ก์ ์กฐํํฉ๋๋ค.
- * @param userId - ์ฌ์ฉ์์ ID
- * @returns ์ฌ์ฉ์์ ์ข์์ํ ๊ฒ์๊ธ ๋ชฉ๋ก
- */
-export async function getMyLikedPostsService(
- userId: string
-): Promise> {
- try {
- const likes = await prisma.postLike.findMany({
- where: { userId },
- include: { post: { include: postIncludeArgs } },
- orderBy: { createdAt: "desc" },
- });
- return serviceSuccess(likes.map((like) => like.post));
- } catch (error) {
- return serviceInternalError(error);
- }
-}
-
-export async function getMyProfileService(
- id: string
-): Promise> {
- try {
- const user = await prisma.user.findUnique({
- where: { id },
- select: {
- id: true,
- name: true,
- nickname: true,
- age: true,
- gender: true,
- imageUrl: true,
- totalPoints: true,
- consecutiveLoginDays: true,
- },
- });
-
- if (!user) {
- return serviceNotFound("์ฌ์ฉ์๋ฅผ ์ฐพ์ ์ ์์ต๋๋ค.");
- }
-
- // ํฌ์ธํธ ๊ธฐ๋ฐ์ผ๋ก ๋ ๋ฒจ ๊ณ์ฐ
- const level = calculateLevel(user.totalPoints);
- const nextLevelPoints = getPointsForNextLevel(user.totalPoints);
-
- return serviceSuccess({
- ...user,
- level,
- nextLevelPoints,
- });
- } catch (error) {
- return serviceInternalError(error);
- }
-}
-
-export async function updateMyProfileService(
- formData: ApiUpdateMyProfileRequest & { id: string }
-): Promise> {
- try {
- const { id, ...updateData } = formData;
-
- const user = await prisma.user.findUnique({
- where: { id },
- select: { imageUrl: true },
- });
-
- if (!user) {
- return serviceNotFound("์ฌ์ฉ์๋ฅผ ์ฐพ์ ์ ์์ต๋๋ค.");
- }
-
- // ๊ธฐ์กด ํ๋กํ ์ด๋ฏธ์ง๊ฐ ์๋ค๋ฉด ์ญ์
- if (user.imageUrl && user.imageUrl.includes(PROFILE_BUCKET_NAME)) {
- await deleteImageByUrl(PROFILE_BUCKET_NAME, user.imageUrl);
- }
- // undefined ๊ฐ ์ ๊ฑฐ
- const cleanedData = ApiUpdateMyProfileRequestSchema.partial()
- .strip()
- .parse(updateData);
-
- const updatedUser = await prisma.user.update({
- where: { id },
- data: cleanedData,
- select: {
- id: true,
- name: true,
- nickname: true,
- age: true,
- gender: true,
- imageUrl: true,
- totalPoints: true,
- consecutiveLoginDays: true,
- },
- });
-
- // ํฌ์ธํธ ๊ธฐ๋ฐ์ผ๋ก ๋ ๋ฒจ ๊ณ์ฐ
- const level = calculateLevel(updatedUser.totalPoints);
- const nextLevelPoints = getPointsForNextLevel(updatedUser.totalPoints);
-
- return serviceSuccess({
- ...updatedUser,
- level,
- nextLevelPoints,
- });
- } catch (error) {
- return serviceInternalError(error);
- }
-}
-
-export type { MyComment, MyCollection };
diff --git a/src/lib/hono/services/point.service.ts b/src/lib/hono/services/point.service.ts
deleted file mode 100644
index 6f592432..00000000
--- a/src/lib/hono/services/point.service.ts
+++ /dev/null
@@ -1,371 +0,0 @@
-import { PointActivityType } from "@prisma/client";
-import { prisma } from "@/lib/prisma";
-import {
- serviceInternalError,
- serviceNotFound,
- ServiceResult,
- serviceSuccess,
- serviceError,
-} from "../utils/service.utils";
-import { POINT_POLICY, POINT_DESCRIPTIONS } from "@/lib/constants/point";
-import type {
- PointEarnData,
- PointHistoryData,
- UserPointsData,
- PointStatisticsData,
-} from "@/lib/types/point.types";
-import dayjs from "dayjs";
-
-/**
- * ํด๋ผ์ด์ธํธ ํ์์คํฌํ ๊ฒ์ฆ
- * @param clientTimestamp - ํด๋ผ์ด์ธํธ์์ ์ ์กํ ํ์์คํฌํ
- * @param maxDiffMs - ํ์ฉ ๊ฐ๋ฅํ ์ต๋ ์๊ฐ ์ฐจ์ด (๋ฐ๋ฆฌ์ด, ๋ฏธ๋ง)
- * @returns ๊ฒ์ฆ ์ฑ๊ณต ์ฌ๋ถ
- */
-export function validateTimestamp(
- clientTimestamp: Date,
- maxDiffMs: number = 5 * 60 * 1000 // ๊ธฐ๋ณธ๊ฐ: 5๋ถ
-): boolean {
- const serverTimestamp = new Date();
- const timeDiff = Math.abs(
- serverTimestamp.getTime() - clientTimestamp.getTime()
- );
- return timeDiff < maxDiffMs;
-}
-
-/**
- * ์ฌ์ฉ์์๊ฒ ํฌ์ธํธ๋ฅผ ์ ๋ฆฝํ๋ ์๋น์ค
- * @param userId - ํฌ์ธํธ๋ฅผ ์ ๋ฆฝํ ์ฌ์ฉ์ ID
- * @param activityType - ํ๋ ํ์
- * @param referenceId - ๊ด๋ จ๋ ๋ฆฌ์์ค ID (๊ฒ์๋ฌผ, ๋๊ธ ๋ฑ)
- * @param description - ์ปค์คํ
์ค๋ช
(์ ํ์ฌํญ)
- * @returns ํฌ์ธํธ ์ ๋ฆฝ ๊ฒฐ๊ณผ
- */
-export async function earnPointsService(
- userId: string,
- activityType: PointActivityType,
- referenceId?: string,
- description?: string
-): Promise> {
- try {
- const pointAmount = POINT_POLICY[activityType];
-
- if (!pointAmount) {
- return serviceError("BAD_REQUEST", "์ ํจํ์ง ์์ ํ๋ ํ์
์
๋๋ค.");
- }
-
- // ์ฌ์ฉ์ ์กด์ฌ ์ฌ๋ถ ํ์ธ
- const userExists = await prisma.user.findUnique({
- where: { id: userId },
- select: { id: true },
- });
-
- if (!userExists) {
- return serviceNotFound("์ฌ์ฉ์๋ฅผ ์ฐพ์ ์ ์์ต๋๋ค.");
- }
-
- // ํธ๋์ญ์
์ผ๋ก ํฌ์ธํธ ์ด๋ ฅ ์์ฑ ๋ฐ ์ด ํฌ์ธํธ ์
๋ฐ์ดํธ
- const result = await prisma.$transaction(async (tx) => {
- // ํฌ์ธํธ ์ด๋ ฅ ์์ฑ
- await tx.pointHistory.create({
- data: {
- userId,
- pointAmount,
- activityType,
- referenceId,
- description: description || POINT_DESCRIPTIONS[activityType],
- },
- });
-
- // ์ฌ์ฉ์ ์ด ํฌ์ธํธ ์
๋ฐ์ดํธ
- const updatedUser = await tx.user.update({
- where: { id: userId },
- data: {
- totalPoints: {
- increment: pointAmount,
- },
- },
- select: {
- totalPoints: true,
- },
- });
-
- return updatedUser;
- });
-
- return serviceSuccess({
- pointAmount,
- totalPoints: result.totalPoints,
- });
- } catch (error) {
- return serviceInternalError(error);
- }
-}
-
-/**
- * ์ฐ์ ๋ก๊ทธ์ธ ์ผ์๋ฅผ ๊ณ์ฐํ๊ณ ํฌ์ธํธ๋ฅผ ์ ๋ฆฝํ๋ ์๋น์ค
- * @param userId - ์ฌ์ฉ์ ID
- * @param currentLoginDate - ํ์ฌ ๋ก๊ทธ์ธ ๋ ์ง
- * @returns ํฌ์ธํธ ์ ๋ฆฝ ๊ฒฐ๊ณผ
- */
-export async function processConsecutiveLoginService(
- userId: string,
- currentLoginDate: Date = new Date()
-): Promise> {
- try {
- // ํ์ํ ๋ชจ๋ ์ ๋ณด๋ฅผ ํ๋ฒ์ ์กฐํ
- const user = await prisma.user.findUnique({
- where: { id: userId },
- select: {
- lastLoginDate: true,
- consecutiveLoginDays: true,
- totalPoints: true,
- },
- });
-
- if (!user) {
- return serviceNotFound("์ฌ์ฉ์๋ฅผ ์ฐพ์ ์ ์์ต๋๋ค.");
- }
-
- const { lastLoginDate, consecutiveLoginDays, totalPoints } = user;
-
- // ์ค๋ ์ด๋ฏธ ๋ก๊ทธ์ธํ ๊ฒฝ์ฐ (DB ์กฐํ๊ฐ ๋ถํ์ํ๋ฏ๋ก ๊ฐ์ฅ ๋จผ์ ์ฒ๋ฆฌ)
- if (lastLoginDate && dayjs(lastLoginDate).isSame(currentLoginDate, "day")) {
- return serviceSuccess({
- pointAmount: 0,
- totalPoints,
- consecutiveLoginDays,
- });
- }
-
- // ์๋ก์ด ์ฐ์ ๋ก๊ทธ์ธ ์ผ์ ๊ณ์ฐ (๋ฉ๋ชจ๋ฆฌ ์์์)
- let newConsecutiveDays = 1;
- const yesterday = dayjs(currentLoginDate).subtract(1, "day");
- if (lastLoginDate && dayjs(lastLoginDate).isSame(yesterday, "day")) {
- newConsecutiveDays = consecutiveLoginDays + 1;
- }
-
- // ์ง๊ธํ ๋ณด์ ๊ฒฐ์
- let activityType: PointActivityType | null = null;
- let customDescription: string | null = null;
-
- if (newConsecutiveDays > 0 && newConsecutiveDays % 7 === 0) {
- activityType = PointActivityType.CONSECUTIVE_LOGIN_7;
- customDescription = `${newConsecutiveDays}์ผ ์ฐ์ ๋ก๊ทธ์ธ ๋ฌ์ฑ!`;
- } else if (newConsecutiveDays === 3) {
- activityType = PointActivityType.CONSECUTIVE_LOGIN_3;
- customDescription = "3์ผ ์ฐ์ ๋ก๊ทธ์ธ ๋ฌ์ฑ!";
- } else if (newConsecutiveDays === 2) {
- activityType = PointActivityType.CONSECUTIVE_LOGIN_2;
- customDescription = "2์ผ ์ฐ์ ๋ก๊ทธ์ธ ๋ฌ์ฑ!";
- }
-
- // DB ์
๋ฐ์ดํธ์ ํฌ์ธํธ ์ง๊ธ์ ํธ๋์ญ์
์ผ๋ก ์ฒ๋ฆฌ
- const earnedPointData = await prisma.$transaction(async (tx) => {
- // ์ฌ์ฉ์ ์ฐ์ ๋ก๊ทธ์ธ ์ ๋ณด ์
๋ฐ์ดํธ
- await tx.user.update({
- where: { id: userId },
- data: {
- lastLoginDate: currentLoginDate,
- consecutiveLoginDays: newConsecutiveDays,
- },
- });
-
- // ๋ณด์์ด ์๋ ๊ฒฝ์ฐ, ํฌ์ธํธ ์ง๊ธ ์ฒ๋ฆฌ (ํธ๋์ญ์
๋ด๋ถ์์ ์ง์ ์คํ)
- if (activityType) {
- const pointAmount = POINT_POLICY[activityType];
-
- // ํฌ์ธํธ ์ด๋ ฅ ์์ฑ
- await tx.pointHistory.create({
- data: {
- userId,
- pointAmount,
- activityType,
- referenceId: undefined,
- description: customDescription || POINT_DESCRIPTIONS[activityType],
- },
- });
-
- // ์ฌ์ฉ์ ์ด ํฌ์ธํธ ์
๋ฐ์ดํธ
- const updatedUser = await tx.user.update({
- where: { id: userId },
- data: {
- totalPoints: {
- increment: pointAmount,
- },
- },
- select: {
- totalPoints: true,
- },
- });
-
- return {
- pointAmount,
- totalPoints: updatedUser.totalPoints,
- consecutiveLoginDays: newConsecutiveDays,
- };
- }
-
- return null; // ๋ณด์์ด ์๋ ๊ฒฝ์ฐ null ๋ฐํ
- });
-
- // 5. ํธ๋์ญ์
๊ฒฐ๊ณผ์ ๋ฐ๋ผ ์ต์ข
์๋ต ์์ฑ
- if (earnedPointData) {
- // ํฌ์ธํธ๊ฐ ์ง๊ธ๋ ๊ฒฝ์ฐ
- return serviceSuccess(earnedPointData);
- } else {
- // ํฌ์ธํธ ์ง๊ธ ์์ด ์ฐ์ ๋ก๊ทธ์ธ ์ ๋ณด๋ง ๊ฐฑ์ ๋ ๊ฒฝ์ฐ
- return serviceSuccess({
- pointAmount: 0,
- totalPoints: totalPoints, // ๋ฏธ๋ฆฌ ์กฐํํด๋ ๊ฐ ์ฌ์ฉ
- consecutiveLoginDays: newConsecutiveDays,
- });
- }
- } catch (error) {
- // findUnique ์คํจ ๋๋ ํธ๋์ญ์
์คํจ ์ ์ฌ๊ธฐ์ ์ฒ๋ฆฌ
- return serviceInternalError(error);
- }
-}
-
-/**
- * ์ฌ์ฉ์์ ํฌ์ธํธ ์ด๋ ฅ์ ์กฐํํ๋ ์๋น์ค (์ปค์ ๊ธฐ๋ฐ ํ์ด์ง๋ค์ด์
)
- * @param userId - ์ฌ์ฉ์ ID
- * @param options - ์กฐํ ์ต์
- * @returns ํฌ์ธํธ ์ด๋ ฅ ๋ชฉ๋ก
- */
-export async function getPointHistoryService(
- userId: string,
- options?: {
- cursor?: string;
- limit?: number;
- activityType?: PointActivityType;
- }
-): Promise> {
- try {
- const { cursor, limit = 20, activityType } = options || {};
-
- // ์ฌ์ฉ์ ์กด์ฌ ์ฌ๋ถ ํ์ธ
- const userExists = await prisma.user.findUnique({
- where: { id: userId },
- select: { id: true },
- });
-
- if (!userExists) {
- return serviceNotFound("์ฌ์ฉ์๋ฅผ ์ฐพ์ ์ ์์ต๋๋ค.");
- }
-
- // limit + 1๊ฐ๋ฅผ ์กฐํํ์ฌ ๋ค์ ํ์ด์ง ์กด์ฌ ์ฌ๋ถ ํ์ธ
- const histories = await prisma.pointHistory.findMany({
- where: {
- userId,
- ...(activityType && { activityType }),
- ...(cursor && {
- id: {
- lt: cursor,
- },
- }),
- },
- orderBy: {
- createdAt: "desc",
- },
- take: limit + 1,
- });
-
- // ๋ค์ ํ์ด์ง๊ฐ ์๋์ง ํ์ธ
- const hasNextPage = histories.length > limit;
- const resultHistories = hasNextPage ? histories.slice(0, limit) : histories;
- const nextCursor = hasNextPage
- ? resultHistories[resultHistories.length - 1].id
- : null;
-
- // ์ ์ฒด ๊ฐ์ ์กฐํ
- const totalCount = await prisma.pointHistory.count({
- where: {
- userId,
- ...(activityType && { activityType }),
- },
- });
-
- return serviceSuccess({
- histories: resultHistories,
- totalCount,
- nextCursor,
- });
- } catch (error) {
- return serviceInternalError(error);
- }
-}
-
-/**
- * ์ฌ์ฉ์์ ํ์ฌ ํฌ์ธํธ๋ฅผ ์กฐํํ๋ ์๋น์ค
- * @param userId - ์ฌ์ฉ์ ID
- * @returns ํ์ฌ ํฌ์ธํธ
- */
-export async function getUserPointsService(
- userId: string
-): Promise> {
- try {
- const user = await prisma.user.findUnique({
- where: { id: userId },
- select: {
- totalPoints: true,
- consecutiveLoginDays: true,
- },
- });
-
- if (!user) {
- return serviceNotFound("์ฌ์ฉ์๋ฅผ ์ฐพ์ ์ ์์ต๋๋ค.");
- }
-
- return serviceSuccess({
- totalPoints: user.totalPoints,
- consecutiveLoginDays: user.consecutiveLoginDays,
- });
- } catch (error) {
- return serviceInternalError(error);
- }
-}
-
-/**
- * ํ๋ ํ์
๋ณ ํฌ์ธํธ ์ ๋ฆฝ ํต๊ณ๋ฅผ ์กฐํํ๋ ์๋น์ค
- * @param userId - ์ฌ์ฉ์ ID
- * @returns ํ๋ ํ์
๋ณ ํต๊ณ
- */
-export async function getPointStatisticsService(
- userId: string
-): Promise> {
- try {
- // ์ฌ์ฉ์ ์กด์ฌ ์ฌ๋ถ ํ์ธ
- const userExists = await prisma.user.findUnique({
- where: { id: userId },
- select: { id: true },
- });
-
- if (!userExists) {
- return serviceNotFound("์ฌ์ฉ์๋ฅผ ์ฐพ์ ์ ์์ต๋๋ค.");
- }
-
- const statistics = await prisma.pointHistory.groupBy({
- by: ["activityType"],
- where: {
- userId,
- },
- _sum: {
- pointAmount: true,
- },
- _count: {
- id: true,
- },
- });
-
- return serviceSuccess({
- statistics: statistics.map((stat) => ({
- activityType: stat.activityType,
- totalPoints: stat._sum.pointAmount || 0,
- count: stat._count.id,
- })),
- });
- } catch (error) {
- return serviceInternalError(error);
- }
-}
diff --git a/src/lib/hono/services/review.service.ts b/src/lib/hono/services/review.service.ts
deleted file mode 100644
index 080613f6..00000000
--- a/src/lib/hono/services/review.service.ts
+++ /dev/null
@@ -1,359 +0,0 @@
-import { Prisma } from "@prisma/client";
-import { prisma } from "@/lib/prisma";
-import type {
- CreateReviewInput,
- UpdateReviewInput,
-} from "../schemas/review.schema";
-import {
- ServiceResult,
- serviceSuccess,
- serviceNotFound,
- serviceAlreadyExists,
- serviceInternalError,
-} from "../utils/service.utils";
-import { checkResourceExists, validateUuid } from "../utils/service.utils";
-import { createCursorPaginationResult } from "../utils/pagination.utils";
-import { reviewIncludeArgs, FullReview } from "../utils/prisma.utils";
-
-const POPULAR_REVIEW_LIMIT = 5;
-const POPULAR_REVIEW_POOL_SIZE = 20;
-
-let optionMap = new Map();
-
-async function initializeOptionMap() {
- try {
- const allOptions = await prisma.attributeOption.findMany({
- include: { attribute: true },
- });
- const newMap = new Map();
- allOptions.forEach((opt) => {
- const key = `${opt.attribute.key}-${opt.value}`;
- newMap.set(key, opt.id);
- });
- optionMap = newMap;
- } catch (error) {
- console.error("Failed to initialize AttributeOption map:", error);
- }
-}
-// ์๋ฒ ์์ ์ ํ๋ฒ๋ง ์คํ๋๋๋ก ํธ์ถ
-initializeOptionMap();
-
-const DEFAULT_REVIEW_LIMIT = 12;
-
-function getOptionIdsFromAttributes(
- attributes: CreateReviewInput["attributes"]
-): number[] {
- const selectionIds: number[] = [];
- for (const [key, value] of Object.entries(attributes)) {
- if (!value) continue;
- const processValue = (val: string) => {
- const mapKey = `${key}-${val}`;
- const id = optionMap.get(mapKey);
- if (id) selectionIds.push(id);
- };
- if (Array.isArray(value)) {
- value.forEach(processValue);
- } else {
- processValue(value as string);
- }
- }
- return selectionIds;
-}
-
-// --- Prisma ์ฟผ๋ฆฌ ํจ์๋ค ---
-async function findReviews(where: Prisma.ReviewWhereInput) {
- return prisma.review.findMany({
- where,
- orderBy: { createdAt: "desc" },
- include: reviewIncludeArgs,
- });
-}
-
-async function findPaginatedReviews(params: {
- where: Prisma.ReviewWhereInput;
- limit: number;
- cursor?: string;
-}) {
- return prisma.review.findMany({
- where: params.where,
- orderBy: { createdAt: "desc" },
- take: params.limit,
- cursor: params.cursor ? { id: params.cursor } : undefined,
- skip: params.cursor ? 1 : 0,
- include: reviewIncludeArgs,
- });
-}
-
-async function countReviews(where: Prisma.ReviewWhereInput) {
- return prisma.review.count({ where });
-}
-
-// --- ์๋น์ค ํจ์๋ค ---
-export async function getReviewsByPerfumeIdService(
- perfumeId: string
-): Promise> {
- try {
- const perfumeCheck = await checkResourceExists(
- "perfume",
- perfumeId,
- "ํฅ์"
- );
- if (!perfumeCheck.success) return perfumeCheck;
-
- const reviews = await findReviews({ perfumeId });
- return serviceSuccess(reviews);
- } catch (error) {
- return serviceInternalError(error);
- }
-}
-
-export async function getPaginatedReviewsByPerfumeIdService(
- perfumeId: string,
- options: { limit?: number; cursor?: string } = {}
-) {
- try {
- const perfumeCheck = await checkResourceExists(
- "perfume",
- perfumeId,
- "ํฅ์"
- );
- if (!perfumeCheck.success) return perfumeCheck;
-
- const limit = options.limit ?? DEFAULT_REVIEW_LIMIT;
- const fetchLimit = limit + 1;
-
- const [reviews, totalCount] = await Promise.all([
- findPaginatedReviews({
- where: { perfumeId },
- limit: fetchLimit,
- cursor: options.cursor,
- }),
- countReviews({ perfumeId }),
- ]);
-
- const paginatedResult = createCursorPaginationResult(
- reviews,
- totalCount,
- limit
- );
- return serviceSuccess(paginatedResult);
- } catch (error) {
- return serviceInternalError(error);
- }
-}
-
-export async function createReviewService(
- payload: CreateReviewInput & { authorId: string; perfumeId: string }
-): Promise> {
- const { authorId, perfumeId, content, usageStatus, attributes } = payload;
- try {
- const [perfumeCheck, userCheck] = await Promise.all([
- checkResourceExists("perfume", perfumeId, "ํฅ์"),
- checkResourceExists("user", authorId, "์ฌ์ฉ์"),
- ]);
- if (!perfumeCheck.success) return perfumeCheck;
- if (!userCheck.success) return userCheck;
-
- const existingReview = await prisma.review.findFirst({
- where: { perfumeId, authorId },
- });
- if (existingReview) {
- return serviceAlreadyExists("์ด๋ฏธ ์ด ํฅ์์ ๋ํ ๋ฆฌ๋ทฐ๋ฅผ ์์ฑํ์
จ์ต๋๋ค.");
- }
-
- const selectionIds = getOptionIdsFromAttributes(attributes);
-
- // ํธ๋์ญ์
์์
- const newReview = await prisma.$transaction(async (tx) => {
- // 1. ํต์ฌ Review ๋ ์ฝ๋ ์์ฑ
- const createdReview = await tx.review.create({
- data: { content, usageStatus, perfumeId, authorId },
- });
-
- // 2. ReviewAttributeSelection ๋ ์ฝ๋๋ค ์์ฑ
- await tx.reviewAttributeSelection.createMany({
- data: selectionIds.map((optionId) => ({
- reviewId: createdReview.id,
- attributeOptionId: optionId,
- })),
- });
-
- // 3. ์์ฑ๋ ๋ฆฌ๋ทฐ ๋ฐ์ดํฐ๋ฅผ ์กฐํํ์ฌ ๋ฐํ
- return tx.review.findUniqueOrThrow({
- where: { id: createdReview.id },
- include: reviewIncludeArgs,
- });
- });
-
- return serviceSuccess(newReview);
- } catch (error) {
- return serviceInternalError(error);
- }
-}
-
-export async function updateReviewService(
- reviewId: string,
- authorId: string,
- updateData: UpdateReviewInput
-): Promise> {
- try {
- const uuidValidation = validateUuid(reviewId, "๋ฆฌ๋ทฐ");
- if (!uuidValidation.success) return uuidValidation;
-
- const review = await prisma.review.findFirst({
- where: { id: reviewId, authorId },
- });
- if (!review) {
- return serviceNotFound("๋ฆฌ๋ทฐ๋ฅผ ์ฐพ์ ์ ์๊ฑฐ๋ ์์ ๊ถํ์ด ์์ต๋๋ค.");
- }
-
- const { content, usageStatus, attributes } = updateData;
-
- const updatedReview = await prisma.$transaction(async (tx) => {
- if (content || usageStatus) {
- await tx.review.update({
- where: { id: reviewId },
- data: {
- content,
- usageStatus,
- },
- });
- }
-
- if (attributes) {
- await tx.reviewAttributeSelection.deleteMany({ where: { reviewId } });
- const selectionIds = getOptionIdsFromAttributes(
- attributes as CreateReviewInput["attributes"]
- );
-
- if (selectionIds.length > 0) {
- await tx.reviewAttributeSelection.createMany({
- data: selectionIds.map((optionId) => ({
- reviewId: reviewId,
- attributeOptionId: optionId,
- })),
- });
- }
- }
-
- return await tx.review.findUniqueOrThrow({
- where: { id: reviewId },
- include: reviewIncludeArgs,
- });
- });
- return serviceSuccess(updatedReview);
- } catch (error) {
- return serviceInternalError(error);
- }
-}
-
-export async function deleteReviewService(
- reviewId: string,
- authorId: string
-): Promise> {
- try {
- const uuidValidation = validateUuid(reviewId, "๋ฆฌ๋ทฐ");
- if (!uuidValidation.success) return uuidValidation;
-
- const review = await prisma.review.findFirst({
- where: { id: reviewId, authorId },
- });
- if (!review) {
- return serviceNotFound("๋ฆฌ๋ทฐ๋ฅผ ์ฐพ์ ์ ์๊ฑฐ๋ ์ญ์ ๊ถํ์ด ์์ต๋๋ค.");
- }
-
- await prisma.review.delete({
- where: { id: reviewId },
- });
- return serviceSuccess({ message: "๋ฆฌ๋ทฐ๊ฐ ์ฑ๊ณต์ ์ผ๋ก ์ญ์ ๋์์ต๋๋ค." });
- } catch (error) {
- return serviceInternalError(error);
- }
-}
-
-//์์ ๋ฆฌ๋ทฐ ์กฐํ ์๋น์ค
-async function findPopularReviewsPool(): Promise {
- const popularReviewsWithCount = await prisma.review.findMany({
- include: {
- ...reviewIncludeArgs,
- _count: {
- select: { likes: true },
- },
- },
- orderBy: {
- likes: {
- _count: "desc",
- },
- },
- take: POPULAR_REVIEW_POOL_SIZE,
- });
-
- return popularReviewsWithCount.map((review) => {
- //_count ์ ๊ฑฐ
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
- const { _count, ...restOfReview } = review;
- return restOfReview;
- });
-}
-
-export async function getPopularReviewsService(): Promise<
- ServiceResult
-> {
- try {
- const popularReviews = await findPopularReviewsPool();
-
- if (popularReviews.length <= POPULAR_REVIEW_LIMIT)
- return serviceSuccess(popularReviews);
-
- const shuffled = popularReviews.sort(() => 0.5 - Math.random());
- const selectedReviews = shuffled.slice(0, POPULAR_REVIEW_LIMIT);
-
- return serviceSuccess(selectedReviews);
- } catch (error) {
- return serviceInternalError(error);
- }
-}
-
-//๋ฉ์ธ ๋ฒ ๋์์ ๊ฐ์ ธ์ฌ ๋ฆฌ๋ทฐ
-export async function getOneRandomPopularReviewService(): Promise<
- ServiceResult
-> {
- try {
- const popularReviews = await findPopularReviewsPool();
-
- const randomIndex = Math.floor(Math.random() * popularReviews.length);
- const randomReview = popularReviews[randomIndex];
-
- return serviceSuccess(randomReview);
- } catch (error) {
- return serviceInternalError(error);
- }
-}
-
-export async function toggleLikeService(
- reviewId: string,
- userId: string
-): Promise> {
- try {
- const [reviewCheck, userCheck] = await Promise.all([
- checkResourceExists("review", reviewId, "๋ฆฌ๋ทฐ"),
- checkResourceExists("user", userId, "์ฌ์ฉ์"),
- ]);
- if (!reviewCheck.success) return reviewCheck;
- if (!userCheck.success) return userCheck;
-
- const like = await prisma.reviewLike.findUnique({
- where: { review_likes_user_id_review_id_key: { reviewId, userId } },
- });
-
- if (like) {
- await prisma.reviewLike.delete({ where: { id: like.id } });
- return serviceSuccess({ liked: false });
- } else {
- await prisma.reviewLike.create({ data: { reviewId, userId } });
- return serviceSuccess({ liked: true });
- }
- } catch (error) {
- return serviceInternalError(error);
- }
-}
diff --git a/src/lib/hono/utils/__tests__/level.utils.test.ts b/src/lib/hono/utils/__tests__/level.utils.test.ts
deleted file mode 100644
index ae08d8c1..00000000
--- a/src/lib/hono/utils/__tests__/level.utils.test.ts
+++ /dev/null
@@ -1,84 +0,0 @@
-import { describe, it, expect } from "vitest";
-import {
- calculateLevel,
- getPointsForNextLevel,
- getLevelProgress,
- getPointsForLevel,
- POINTS_PER_LEVEL,
-} from "../../../utils/level.utils";
-
-/**
- * ๋ ๋ฒจ ์ ํธ๋ฆฌํฐ ํจ์ ํ
์คํธ
- *
- * ํ
์คํธ ์ ๋ต:
- * - ๊ฒฝ๊ณ๊ฐ์ ์ค์ฌ์ผ๋ก 3๊ฐ์ฉ ๊ฐ๊ฒฐํ๊ฒ ํ
์คํธ
- * - ์ต์๊ฐ, ๊ฒฝ๊ณ๊ฐ, ์ผ๋ฐ๊ฐ์ผ๋ก ๊ตฌ์ฑ
- */
-
-describe("Level Utils - calculateLevel", () => {
- it("๊ฒฝ๊ณ๊ฐ ํ
์คํธ: 0, 99, 100", () => {
- expect(calculateLevel(0)).toBe(0); // ์ต์๊ฐ
- expect(calculateLevel(99)).toBe(0); // ๋ ๋ฒจ ์ฌ๋ฆฌ๊ธฐ ์ง์
- expect(calculateLevel(100)).toBe(1); // ๋ ๋ฒจ ์
- });
-
- it("์ผ๋ฐ๊ฐ ํ
์คํธ: 150, 1000, ์์", () => {
- expect(calculateLevel(150)).toBe(1); // ์ค๊ฐ๊ฐ
- expect(calculateLevel(1000)).toBe(10); // ํฐ ๊ฐ
- expect(calculateLevel(-100)).toBe(0); // ์์
- });
-});
-
-describe("Level Utils - getPointsForNextLevel", () => {
- it("๊ฒฝ๊ณ๊ฐ ํ
์คํธ: 0, 99, 100", () => {
- expect(getPointsForNextLevel(0)).toBe(100); // ์์์
- expect(getPointsForNextLevel(99)).toBe(1); // ๋ ๋ฒจ ์
์ง์
- expect(getPointsForNextLevel(100)).toBe(100); // ๋ ๋ฒจ ์์์
- });
-
- it("์ผ๋ฐ๊ฐ ํ
์คํธ: 50, 150, ์์", () => {
- expect(getPointsForNextLevel(50)).toBe(50); // ์ค๊ฐ
- expect(getPointsForNextLevel(150)).toBe(50); // ๋ค์ ๋ ๋ฒจ ์ค๊ฐ
- expect(getPointsForNextLevel(-100)).toBe(100); // ์์
- });
-});
-
-describe("Level Utils - getLevelProgress", () => {
- it("๊ฒฝ๊ณ๊ฐ ํ
์คํธ: 0, 99, 100", () => {
- expect(getLevelProgress(0)).toBe(0); // ์์์
- expect(getLevelProgress(99)).toBe(99); // ๊ฑฐ์ ๋
- expect(getLevelProgress(100)).toBe(0); // ๋ค์ ๋ ๋ฒจ ์์
- });
-
- it("์ผ๋ฐ๊ฐ ํ
์คํธ: 50, 150, ์์", () => {
- expect(getLevelProgress(50)).toBe(50); // ์ค๊ฐ
- expect(getLevelProgress(150)).toBe(50); // ๋ค์ ๋ ๋ฒจ ์ค๊ฐ
- expect(getLevelProgress(-100)).toBe(0); // ์์
- });
-});
-
-describe("Level Utils - getPointsForLevel", () => {
- it("๊ฒฝ๊ณ๊ฐ ํ
์คํธ: 0, 1, 10", () => {
- expect(getPointsForLevel(0)).toBe(0); // ์ต์ ๋ ๋ฒจ
- expect(getPointsForLevel(1)).toBe(100); // ์ฒซ ๋ ๋ฒจ
- expect(getPointsForLevel(10)).toBe(1000); // ์ผ๋ฐ ๋ ๋ฒจ
- });
-
- it("ํน์๊ฐ ํ
์คํธ: 100, -1", () => {
- expect(getPointsForLevel(100)).toBe(10000); // ํฐ ๋ ๋ฒจ
- expect(getPointsForLevel(-1)).toBe(0); // ์์ ๋ ๋ฒจ
- });
-});
-
-describe("Level Utils - ํตํฉ ์๋๋ฆฌ์ค", () => {
- it("ํฌ์ธํธ 1050์ผ ๋ ๋ชจ๋ ํจ์๊ฐ ์ผ๊ด๋๊ฒ ๋์", () => {
- const points = 1050;
- expect(calculateLevel(points)).toBe(10);
- expect(getPointsForNextLevel(points)).toBe(50);
- expect(getLevelProgress(points)).toBe(50);
- });
-
- it("์์ ๊ฐ ๊ฒ์ฆ", () => {
- expect(POINTS_PER_LEVEL).toBe(100);
- });
-});
diff --git a/src/lib/hono/utils/prisma.utils.ts b/src/lib/hono/utils/prisma.utils.ts
deleted file mode 100644
index 54749b37..00000000
--- a/src/lib/hono/utils/prisma.utils.ts
+++ /dev/null
@@ -1,214 +0,0 @@
-import { Prisma } from "@prisma/client";
-
-export const brandSelect = {
- nameEn: true,
- nameKo: true,
- brandUrl: true,
-} satisfies Prisma.BrandSelect;
-
-export const authorSelect = {
- id: true,
- nickname: true,
- imageUrl: true,
-} satisfies Prisma.UserSelect;
-
-// Perfume Includes
-export const perfumeBaseInclude = {
- brand: { select: brandSelect },
- perfumeImage: { select: { imageUrl: true } },
-} satisfies Prisma.PerfumeInclude;
-
-export const perfumeDetailInclude = {
- ...perfumeBaseInclude,
- accordMappings: { select: { accord: true } },
- noteMappings: { select: { note: true, noteStage: true } },
- reviews: {
- select: {
- id: true,
- content: true,
- author: { select: authorSelect },
- },
- orderBy: { createdAt: "desc" as const },
- take: 5,
- },
- _count: {
- select: { bookmarks: true, reviews: true, collectedByUsers: true },
- },
-} satisfies Prisma.PerfumeInclude;
-
-// Brand Includes
-export const brandSimpleSelect = {
- nameEn: true,
- nameKo: true,
-} satisfies Prisma.BrandSelect;
-
-export const brandDetailSelect = {
- id: true,
- nameEn: true,
- nameKo: true,
- description: true,
- imageUrl: true,
- brandUrl: true,
- mapLocation: true,
-} satisfies Prisma.BrandSelect;
-
-export const brandWithPerfumesInclude = {
- perfumes: {
- include: perfumeBaseInclude,
- orderBy: { nameKo: "asc" as const },
- },
-} satisfies Prisma.BrandInclude;
-
-// Post Includes
-export const postIncludeArgs = {
- author: {
- select: authorSelect,
- },
-} satisfies Prisma.PostInclude;
-
-export const postDetailIncludeArgs = {
- ...postIncludeArgs,
- perfumeMappings: {
- select: {
- perfume: {
- select: {
- id: true,
- nameEn: true,
- nameKo: true,
- ...perfumeBaseInclude,
- },
- },
- },
- },
-} satisfies Prisma.PostInclude;
-
-// Review Includes
-export const reviewIncludeArgs = {
- author: {
- select: authorSelect,
- },
- perfume: {
- select: {
- id: true,
- nameKo: true,
- nameEn: true,
- ...perfumeBaseInclude,
- },
- },
- attributeSelections: {
- include: {
- option: {
- include: {
- attribute: true,
- },
- },
- },
- },
-} satisfies Prisma.ReviewInclude;
-
-// Comment Includes
-export const commentIncludeArgs = {
- author: {
- select: authorSelect,
- },
- replies: {
- include: {
- author: {
- select: authorSelect,
- },
- },
- orderBy: {
- createdAt: "asc" as const,
- },
- },
-} satisfies Prisma.CommentInclude;
-
-// Collection Includes
-export const myCollectionInclude = {
- image: true,
-} satisfies Prisma.UserCollectionInclude;
-
-export const myCommentInclude = {
- post: { select: { id: true, title: true } },
-} satisfies Prisma.CommentInclude;
-
-export type BasePerfume = Prisma.PerfumeGetPayload<{
- include: typeof perfumeBaseInclude;
-}>;
-
-export type FullPerfume = Prisma.PerfumeGetPayload<{
- include: typeof perfumeDetailInclude;
-}>;
-
-export type UserProfile = Prisma.UserGetPayload<{
- select: typeof authorSelect;
-}>;
-
-export type BasePost = Prisma.PostGetPayload<{
- include: typeof postIncludeArgs;
-}>;
-
-export type FullPost = Prisma.PostGetPayload<{
- include: typeof postDetailIncludeArgs;
-}>;
-
-export type FullReview = Prisma.ReviewGetPayload<{
- include: typeof reviewIncludeArgs;
-}>;
-
-export type CommentWithReplies = Prisma.CommentGetPayload<{
- include: typeof commentIncludeArgs;
-}>;
-
-export type MyCollection = Prisma.UserCollectionGetPayload<{
- include: typeof myCollectionInclude;
-}>;
-
-export type MyComment = Prisma.CommentGetPayload<{
- include: typeof myCommentInclude;
-}>;
-
-// Brand Types
-export type SimpleBrand = Prisma.BrandGetPayload<{
- select: typeof brandSimpleSelect;
-}>;
-
-export type DetailBrand = Prisma.BrandGetPayload<{
- select: typeof brandDetailSelect;
-}>;
-
-export type BrandWithPerfumes = Prisma.BrandGetPayload<{
- include: typeof brandWithPerfumesInclude;
-}>;
-
-// User Collection Types
-export type UserCollectionWithRelations = Prisma.UserCollectionGetPayload<{
- include: { image: true; perfume: true };
-}>;
-
-// ==================== Type Transform Utilities ====================
-
-/**
- * Prisma JsonValue๋ฅผ MapLocation์ผ๋ก ๋ณํ
- * - ์ ํจํ ํ์์ด๋ฉด { latitude, longitude } ๋ฐํ
- * - ์ ํจํ์ง ์์ผ๋ฉด null ๋ฐํ
- */
-export function parseMapLocation(
- value: Prisma.JsonValue
-): { latitude: number; longitude: number } | null {
- if (value === null) return null;
-
- if (typeof value === "object" && value !== null && !Array.isArray(value)) {
- const obj = value as Record;
-
- if (typeof obj.latitude === "number" && typeof obj.longitude === "number") {
- return {
- latitude: obj.latitude,
- longitude: obj.longitude,
- };
- }
- }
-
- console.warn("Invalid mapLocation format:", value);
- return null;
-}
diff --git a/src/lib/hono/utils/service.utils.ts b/src/lib/hono/utils/service.utils.ts
deleted file mode 100644
index ef954152..00000000
--- a/src/lib/hono/utils/service.utils.ts
+++ /dev/null
@@ -1,161 +0,0 @@
-import { prisma } from "@/lib/prisma";
-import { z } from "zod";
-import { Context } from "hono";
-import { ImageFormat } from "@prisma/client";
-import { User } from "next-auth";
-
-const IMAGE_FORMAT_MAP: Record = {
- JPEG: ImageFormat.JPEG,
- JPG: ImageFormat.JPEG,
- PNG: ImageFormat.PNG,
- WEBP: ImageFormat.WEBP,
- HEIF: ImageFormat.HEIC,
- HEIC: ImageFormat.HEIC,
-} as const;
-
-/**
- * ์ธ์ฆ๋ ์ฌ์ฉ์ ํ์
- id๊ฐ ๋ฐ๋์ ์กด์ฌํจ
- */
-export type AuthenticatedUser = Required> & Omit;
-
-export type ServiceErrorCode =
- | "NOT_FOUND"
- | "ALREADY_EXISTS"
- | "BAD_REQUEST"
- | "INTERNAL_ERROR"
- | "FORBIDDEN";
-
-export type ServiceResult =
- | { success: true; data: T }
- | { success: false; error: E; message?: string };
-
-/**
- * ์ฑ๊ณต์ ์ธ ์๋น์ค ๊ฒฐ๊ณผ ๊ฐ์ฒด๋ฅผ ์์ฑํฉ๋๋ค.
- * @param data - ์ฑ๊ณต ์ ๋ฐํํ ๋ฐ์ดํฐ
- */
-export function serviceSuccess(data: T): ServiceResult {
- return { success: true, data };
-}
-
-/**
- * ์คํจ ์๋น์ค ๊ฒฐ๊ณผ ๊ฐ์ฒด๋ฅผ ์์ฑํฉ๋๋ค.
- * @param error - ์๋ฌ๋ฅผ ์๋ณํ๋ ์ฝ๋ ๋๋ ๋ฌธ์์ด
- * @param message - ํด๋ผ์ด์ธํธ์๊ฒ ๋ณด์ฌ์ค ์ ์๋ ์๋ฌ ๋ฉ์์ง
- */
-export function serviceError(
- error: E,
- message?: string
-): ServiceResult {
- return { success: false, error, message };
-}
-
-/**
- * 'NOT_FOUND' ์๋ฌ ๊ฒฐ๊ณผ ๊ฐ์ฒด๋ฅผ ์์ฑํฉ๋๋ค.
- */
-export const serviceNotFound = (message = "๋ฆฌ์์ค๋ฅผ ์ฐพ์ ์ ์์ต๋๋ค.") =>
- serviceError("NOT_FOUND", message);
-
-/**
- * 'ALREADY_EXISTS' ์๋ฌ ๊ฒฐ๊ณผ ๊ฐ์ฒด๋ฅผ ์์ฑํฉ๋๋ค.
- */
-export const serviceAlreadyExists = (message = "์ด๋ฏธ ์กด์ฌํ๋ ๋ฆฌ์์ค์
๋๋ค.") =>
- serviceError("ALREADY_EXISTS", message);
-
-/**
- * 'BAD_REQUEST' ๋๋ ์ ํจ์ฑ ๊ฒ์ฌ ์๋ฌ ๊ฒฐ๊ณผ ๊ฐ์ฒด๋ฅผ ์์ฑํฉ๋๋ค.
- */
-export const serviceBadRequest = (message = "์๋ชป๋ ์์ฒญ์
๋๋ค.") =>
- serviceError("BAD_REQUEST", message);
-
-/**
- * 'FORBIDDEN' ์๋ฌ ๊ฒฐ๊ณผ ๊ฐ์ฒด๋ฅผ ์์ฑํฉ๋๋ค.
- */
-export const serviceForbidden = (message = "์์ฒญ์ ์ํํ ๊ถํ์ด ์์ต๋๋ค.") =>
- serviceError("FORBIDDEN", message);
-
-/**
- * ์๊ธฐ์น ์์ ๋ด๋ถ ์๋ฒ ์ค๋ฅ ๊ฒฐ๊ณผ ๊ฐ์ฒด๋ฅผ ์์ฑํฉ๋๋ค.
- */
-export function serviceInternalError(
- error: unknown
-): ServiceResult {
- const err = error instanceof Error ? error : new Error(String(error));
- const message = err.message;
- console.error("[INTERNAL_ERROR]", err);
- return { success: false, error: "INTERNAL_ERROR", message };
-}
-
-type FindUniqueMethod = {
- findUnique: (args: {
- where: { id: string };
- select: { id: boolean };
- }) => Promise<{ id: string } | null>;
-};
-
-/**
- * ํน์ ๋ฆฌ์์ค๊ฐ ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ์กด์ฌํ๋์ง ํ์ธํฉ๋๋ค.
- * @param model - Prisma ๋ชจ๋ธ ์ด๋ฆ (์: "perfume", "user")
- * @param id - ํ์ธํ ๋ฆฌ์์ค์ ID
- * @param resourceName - ์ค๋ฅ ๋ฉ์์ง์ ์ฌ์ฉํ ๋ฆฌ์์ค์ ํ๊ธ ์ด๋ฆ (์: "ํฅ์")
- */
-export async function checkResourceExists(
- model: keyof typeof prisma,
- id: string,
- resourceName: string
-): Promise> {
- const resource = await (
- prisma[model] as unknown as FindUniqueMethod
- ).findUnique({
- where: { id },
- select: { id: true },
- });
-
- if (!resource) {
- return serviceNotFound(`${resourceName}์(๋ฅผ) ์ฐพ์ ์ ์์ต๋๋ค.`);
- }
- return serviceSuccess(true);
-}
-
-/**
- * UUID ํ์์ด ์ ํจํ์ง Zod๋ฅผ ์ฌ์ฉํ์ฌ ๊ฒ์ฌํฉ๋๋ค.
- * @param id - ๊ฒ์ฌํ ID
- * @param resourceName - ์ค๋ฅ ๋ฉ์์ง์ ์ฌ์ฉํ ๋ฆฌ์์ค์ ์ด๋ฆ
- */
-export function validateUuid(
- id: string,
- resourceName: string
-): ServiceResult {
- const uuidSchema = z
- .string()
- .uuid({ message: `์ ํจํ์ง ์์ ${resourceName} ID ํ์์
๋๋ค.` });
- const result = uuidSchema.safeParse(id);
-
- if (!result.success) {
- return serviceBadRequest(result.error.flatten().formErrors[0]);
- }
- return serviceSuccess(result.data);
-}
-
-/**
- * ์ธ์ฆ๋ ์ฌ์ฉ์ ์ ๋ณด๋ฅผ ๊ฐ์ ธ์ต๋๋ค.
- * @param c - Hono ์ปจํ
์คํธ
- * @returns ์ธ์ฆ๋ ์ฌ์ฉ์ ์ ๋ณด (id๊ฐ ๋ณด์ฅ๋จ)
- * @throws ์ธ์ฆ๋์ง ์์ ์ฌ์ฉ์์ด๊ฑฐ๋ id๊ฐ ์๋ ๊ฒฝ์ฐ ์๋ฌ๋ฅผ throwํฉ๋๋ค
- */
-export function getAuthenticatedUser(c: Context): AuthenticatedUser {
- const user = c.get("user");
- if (!user || !user.id) {
- throw new Error("์ธ์ฆ๋์ง ์์ ์ฌ์ฉ์์
๋๋ค.");
- }
- return user as AuthenticatedUser;
-}
-/**
- * ์ด๋ฏธ์ง ํ์์ ๊ฐ์ ธ์ต๋๋ค.
- * @param formatString - ์ด๋ฏธ์ง ํ์ ๋ฌธ์์ด
- * @returns ์ด๋ฏธ์ง ํ์
- */
-
-export function getImageFormat(formatString?: string): ImageFormat {
- if (!formatString) return ImageFormat.UNKNOWN;
- return IMAGE_FORMAT_MAP[formatString.toUpperCase()] ?? ImageFormat.UNKNOWN;
-}
diff --git a/src/lib/hooks/query/useDraftQuery.ts b/src/lib/hooks/query/useDraftQuery.ts
deleted file mode 100644
index 6741263d..00000000
--- a/src/lib/hooks/query/useDraftQuery.ts
+++ /dev/null
@@ -1,181 +0,0 @@
-import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
-import { draftApi } from "@/lib/utils/api/draft.api";
-import { queryKeys } from "@/lib/utils/queryKeys";
-import { CreateDraftBody } from "@/lib/hono/schemas/draft.schema";
-import { DraftType } from "@prisma/client";
-
-/**
- * ์์ ์ ์ฅ ์์ฑ/์
๋ฐ์ดํธ Mutation Hook
- */
-export const useCreateDraft = () => {
- const queryClient = useQueryClient();
-
- return useMutation({
- mutationFn: (draftData: CreateDraftBody) => draftApi.create(draftData),
- onSuccess: (data) => {
- // ์์ ์ ์ฅ ๋ชฉ๋ก ๋ฌดํจํํ์ฌ ์ต์ ๋ฐ์ดํฐ ๋ฐ์
- queryClient.invalidateQueries({
- queryKey: queryKeys.drafts.list(),
- });
- // ํ์
๋ณ ์ฟผ๋ฆฌ๋ ๋ฌดํจํ
- if (data?.data?.type) {
- queryClient.invalidateQueries({
- queryKey: queryKeys.drafts.byType(data.data.type),
- });
- }
- },
- });
-};
-
-/**
- * ์์ ์ ์ฅ ๋ชฉ๋ก ์กฐํ Query Hook
- */
-export const useDraftList = () => {
- return useQuery({
- queryKey: queryKeys.drafts.list(),
- queryFn: async () => {
- try {
- const response = await draftApi.list();
- if (!response || !response.success) {
- return [];
- }
- return response.data;
- } catch (error) {
- console.error("Failed to fetch draft list:", error);
- return [];
- }
- },
- staleTime: 0,
- gcTime: 0,
- });
-};
-
-/**
- * ํน์ ์์ ์ ์ฅ ์กฐํ Query Hook
- */
-export const useDraft = (id: string) => {
- return useQuery({
- queryKey: queryKeys.drafts.detail(id),
- queryFn: async () => {
- try {
- const response = await draftApi.get(id);
- if (!response || !response.success) {
- return null;
- }
- return response.data;
- } catch (error) {
- console.error("Failed to fetch draft:", error);
- return null;
- }
- },
- enabled: !!id,
- staleTime: 0,
- gcTime: 0,
- });
-};
-
-/**
- * ์์ ์ ์ฅ ์ญ์ Mutation Hook
- */
-export const useDeleteDraft = () => {
- const queryClient = useQueryClient();
-
- return useMutation({
- mutationFn: (id: string) => {
- return draftApi.delete(id);
- },
- onSuccess: (_data, deletedId) => {
- console.log("[useDeleteDraft] ์ญ์ ์ฑ๊ณต:", deletedId);
-
- // ๋ชจ๋ drafts ๊ด๋ จ ์ฟผ๋ฆฌ ๋ฌดํจํ (๊ฐ์ฅ ๊ฐ๋ ฅํ ๋ฐฉ๋ฒ)
- queryClient.invalidateQueries({
- queryKey: queryKeys.drafts.all,
- });
-
- // ์ถ๊ฐ๋ก ๋ช
์์ ์ผ๋ก ๊ฐ ์ฟผ๋ฆฌ ๋ฌดํจํ
- queryClient.invalidateQueries({
- queryKey: queryKeys.drafts.list(),
- });
-
- // UPDATE ํ์
์ฟผ๋ฆฌ ๋ฌดํจํ
- queryClient.invalidateQueries({
- queryKey: queryKeys.drafts.byType(DraftType.UPDATE),
- });
-
- // ๋ชจ๋ byPostId ์ฟผ๋ฆฌ ์ ๊ฑฐ (ํจํด ๋งค์นญ)
- queryClient.removeQueries({
- predicate: (query) => {
- const key = query.queryKey;
- return (
- Array.isArray(key) && key[0] === "drafts" && key[1] === "postId"
- );
- },
- });
- },
- onError: (error) => {
- console.error("[useDeleteDraft] ์ญ์ ์คํจ:", error);
- },
- });
-};
-
-/**
- * ํ์
๋ณ ์์ ์ ์ฅ ์กฐํ Query Hook
- * @param type - CREATE: ์ ๊ธ ์์ฑ์ฉ, UPDATE: ๊ธ ์์ ์ฉ
- * @param enabled - ์ฟผ๋ฆฌ ํ์ฑํ ์ฌ๋ถ (๊ธฐ๋ณธ๊ฐ: true)
- */
-export const useDraftByType = (type: DraftType, enabled: boolean = true) => {
- return useQuery({
- queryKey: queryKeys.drafts.byType(type),
- queryFn: async () => {
- try {
- const response = await draftApi.list();
- if (!response || !response.success) {
- return null;
- }
- // ํ์
์ ๋ง๋ draft ์ฐพ๊ธฐ (userId, type unique constraint๋ก 1๊ฐ๋ง ์กด์ฌ)
- const draft = response.data.find((d) => d.type === type);
- return draft || null;
- } catch (error) {
- console.error(`Failed to fetch ${type} draft:`, error);
- return null;
- }
- },
- enabled,
- staleTime: 0,
- gcTime: 0,
- });
-};
-
-/**
- * postId๋ก UPDATE ํ์
์์ ์ ์ฅ ์กฐํ Query Hook
- * @param postId - ๊ฒ์๊ธ ID
- * @param enabled - ์ฟผ๋ฆฌ ํ์ฑํ ์ฌ๋ถ (๊ธฐ๋ณธ๊ฐ: true)
- */
-export const useDraftByPostId = (postId: string, enabled: boolean = true) => {
- return useQuery({
- queryKey: queryKeys.drafts.byPostId(postId),
- queryFn: async () => {
- console.log(`[useDraftByPostId] Fetch ์์ for postId: ${postId}`);
- try {
- const response = await draftApi.list();
- if (!response || !response.success) {
- console.log(`[useDraftByPostId] API ์๋ต ์คํจ`);
- return null;
- }
-
- // UPDATE ํ์
์ด๋ฉด์ ํด๋น postId๋ฅผ ๊ฐ์ง draft ์ฐพ๊ธฐ
- const draft = response.data.find(
- (d) => d.type === DraftType.UPDATE && d.postId === postId,
- );
-
- return draft || null;
- } catch (error) {
- console.error(`[useDraftByPostId] ์ค๋ฅ for postId ${postId}:`, error);
- return null;
- }
- },
- enabled: enabled && !!postId,
- staleTime: 0,
- gcTime: 0,
- });
-};
diff --git a/src/lib/hooks/useIntersectionObserver.ts b/src/lib/hooks/useIntersectionObserver.ts
deleted file mode 100644
index 31212ced..00000000
--- a/src/lib/hooks/useIntersectionObserver.ts
+++ /dev/null
@@ -1,33 +0,0 @@
-import { useEffect, useState } from "react";
-
-export function useIntersectionObserver(
- ref: React.RefObject,
- threshold: number = 0.1
-) {
- const [isVisible, setIsVisible] = useState(false);
-
- useEffect(() => {
- const targetElement = ref.current;
-
- if (!targetElement) return;
-
- const observer = new IntersectionObserver(
- (entries) => {
- entries.forEach((entry) => {
- setIsVisible(entry.isIntersecting);
- });
- },
- { threshold }
- );
-
- observer.observe(targetElement);
-
- return () => {
- if (targetElement) {
- observer.unobserve(targetElement);
- }
- };
- }, [ref, threshold]);
-
- return isVisible;
-}
diff --git a/src/lib/mocks/communityCard.ts b/src/lib/mocks/communityCard.ts
deleted file mode 100644
index 9fe1e087..00000000
--- a/src/lib/mocks/communityCard.ts
+++ /dev/null
@@ -1,483 +0,0 @@
-import { PostMetaItem } from "@/components/commons/author/author.types";
-import { CategoryType } from "@/lib/constants/post";
-
-export type TCommunityPost = {
- id: string;
- title: string;
- content: string;
- author: string;
- createdAt: string;
- categoryType: CategoryType;
- meta: PostMetaItem[];
-};
-
-export const mockCommunityPostData: TCommunityPost[] = [
- {
- id: "1",
- title: "๋ด์ ์ด์ธ๋ฆฌ๋ ํฅ์ ์ถ์ฒ",
- content: "๋ฐ๋ปํ ๋ด๋ ์ ์ ์ด์ธ๋ฆฌ๋ ํ๋ก๋ด ๊ณ์ด ํฅ์๋ฅผ ์ถ์ฒํด์.",
- author: "ํฅ๊ธฐ๋ก์ด๋",
- createdAt: "2025-04-25 09:12:00",
- categoryType: "RECOMMENDATION",
- meta: [
- { type: "Like", count: 12 },
- { type: "Comment", count: 3 },
- { type: "View", count: 157 },
- ],
- },
- {
- id: "2",
- title: "ํฅ์ ์
๋ฌธ์ ์ง๋ฌธ ์์ด์",
- content: "์ฒ์ ํฅ์๋ฅผ ์ฌ๋ ค๋๋ฐ ์ด๋ค ์ ํ์ด ์ข์๊น์?",
- author: "ํผํธ์ด๋ณด",
- createdAt: "2025-04-24 14:30:00",
- categoryType: "QUESTION",
- meta: [
- { type: "Like", count: 5 },
- { type: "Comment", count: 7 },
- { type: "View", count: 89 },
- ],
- },
- {
- id: "3",
- title: "์์ ๋กญ๊ฒ ์ด์ผ๊ธฐํด์",
- content: "ํฅ์ ์ธ์๋ ๋ค์ํ ์ด์ผ๊ธฐ๋ฅผ ๋๋ ์! ์ค๋ ๋ ์จ ๋๋ฌด ์ข๋ค์.",
- author: "์์ ์ธ",
- createdAt: "2025-04-23 18:45:00",
- categoryType: "FREEBOARD",
- meta: [
- { type: "Like", count: 9 },
- { type: "Comment", count: 2 },
- { type: "View", count: 45 },
- ],
- },
- {
- id: "4",
- title: "์ฌ๋ฆ์ฒ ์์ํ ํฅ์",
- content: "์ฌ๋ฆ์ ์ด์ธ๋ฆฌ๋ ์ํธ๋ฌ์ค ๊ณ์ด ํฅ์ ์ถ์ฒ ๋ถํ๋๋ ค์.",
- author: "์์ํํฅ๊ธฐ",
- createdAt: "2025-04-22 11:20:00",
- categoryType: "RECOMMENDATION",
- meta: [
- { type: "Like", count: 15 },
- { type: "Comment", count: 4 },
- { type: "View", count: 110 },
- ],
- },
- {
- id: "5",
- title: "ํฅ์ ์ถ์ฒ ๋ถํ๋๋ฆฝ๋๋ค",
- content: "20๋ ์ฌ์ฑ์๊ฒ ์ด์ธ๋ฆฌ๋ ํฅ์ ์ถ์ฒํด์ฃผ์ธ์.",
- author: "์ถ์ฒ์์ ",
- createdAt: "2025-04-21 20:05:00",
- categoryType: "QUESTION",
- meta: [
- { type: "Like", count: 8 },
- { type: "Comment", count: 6 },
- { type: "View", count: 99 },
- ],
- },
- {
- id: "6",
- title: "์ค๋์ ์์ ๊ฒ์ํ",
- content: "์ค๋ ํ๋ฃจ๋ ๋ชจ๋ ํ๋ด์ธ์! ์์ ๊ฒ์ํ์์ ์ํตํด์.",
- author: "์ค๋๋์์ ",
- createdAt: "2025-04-20 08:55:00",
- categoryType: "FREEBOARD",
- meta: [
- { type: "Like", count: 6 },
- { type: "Comment", count: 1 },
- { type: "View", count: 38 },
- ],
- },
- {
- id: "7",
- title: "๊ฒจ์ธ์ ์ด์ธ๋ฆฌ๋ ํฅ์",
- content: "๊ฒจ์ธ์๋ ๋จธ์คํฌ๋ ์ฐ๋ ๊ณ์ด์ด ์ ์ด์ธ๋ฆฌ๋ ๊ฒ ๊ฐ์์.",
- author: "๊ฒจ์ธํฅ๊ธฐ",
- createdAt: "2025-04-19 17:40:00",
- categoryType: "RECOMMENDATION",
- meta: [
- { type: "Like", count: 11 },
- { type: "Comment", count: 2 },
- { type: "View", count: 131 },
- ],
- },
- {
- id: "8",
- title: "ํฅ์ ๋ ์ด์ด๋ง ํ ์๋ ค์ค์",
- content: "๋ ๊ฐ์ง ์ด์์ ํฅ์๋ฅผ ์์ด ์ฐ๋ ๋ฐฉ๋ฒ์ด ๊ถ๊ธํฉ๋๋ค.",
- author: "๋ ์ด์ด๋ง์ด๋ณด",
- createdAt: "2025-04-18 13:15:00",
- categoryType: "QUESTION",
- meta: [
- { type: "Like", count: 7 },
- { type: "Comment", count: 5 },
- { type: "View", count: 74 },
- ],
- },
- {
- id: "9",
- title: "์์ ๊ฒ์ํ์ ์ค์ ๊ฑธ ํ์",
- content: "์์ ๋กญ๊ฒ ๊ธ์ ๋จ๊ฒจ์ฃผ์ธ์. ๊ท์น๋ง ์ง์ผ์ฃผ์๋ฉด ๋ฉ๋๋ค!",
- author: "์ด์์",
- createdAt: "2025-04-17 10:10:00",
- categoryType: "FREEBOARD",
- meta: [
- { type: "Like", count: 10 },
- { type: "Comment", count: 0 },
- { type: "View", count: 60 },
- ],
- },
- {
- id: "10",
- title: "๊ฐ์ฑ๋น ์ข์ ํฅ์ ๋ชจ์",
- content: "๊ฐ๊ฒฉ๋๋น ๋ง์กฑ๋๊ฐ ๋์ ํฅ์ ๋ฆฌ์คํธ ๊ณต์ ํฉ๋๋ค.",
- author: "๊ฐ์ฑ๋นํผํธ",
- createdAt: "2025-04-16 19:30:00",
- categoryType: "RECOMMENDATION",
- meta: [
- { type: "Like", count: 13 },
- { type: "Comment", count: 3 },
- { type: "View", count: 120 },
- ],
- },
- {
- id: "11",
- title: "ํฅ์ ๊ณต๋ณ ์ธ์ฆํฉ๋๋ค",
- content: "๋๋์ด ํฅ์ ๊ณต๋ณ์ด ๋์์ด์! ๋ค ์ฐ๊ณ ๋๋ ๋ฟ๋ฏํ๋ค์.",
- author: "๊ณต๋ณ๋ถ์",
- createdAt: "2025-04-15 08:55:00",
- categoryType: "FREEBOARD",
- meta: [
- { type: "Like", count: 5 },
- { type: "Comment", count: 1 },
- { type: "View", count: 32 },
- ],
- },
- {
- id: "12",
- title: "ํฅ์ ๊ณ ๋ฅด๋ ๊ธฐ์ค ๊ถ๊ธํด์",
- content: "ํฅ์ ๊ณ ๋ฅผ ๋ ๊ฐ์ฅ ์ค์ํ๊ฒ ์๊ฐํ๋ ๊ธฐ์ค์ด ๋ญ๊ฐ์?",
- author: "์ ํ์ฅ์ ",
- createdAt: "2025-04-14 09:00:00",
- categoryType: "QUESTION",
- meta: [
- { type: "Like", count: 4 },
- { type: "Comment", count: 2 },
- { type: "View", count: 41 },
- ],
- },
- {
- id: "13",
- title: "๋จ์ ํฅ์ ๋ฒ ์คํธ TOP 3",
- content: "๋จ์๋ค์ด ๋ง์ด ์ฐ๋ ํฅ์ ๋ฒ ์คํธ 3๋ฅผ ์ ๋ฆฌํด๋ดค์ด์. ์ฐธ๊ณ ํ์ธ์!",
- author: "ํฅ์๋จ",
- createdAt: "2025-04-13 20:05:00",
- categoryType: "RECOMMENDATION",
- meta: [
- { type: "Like", count: 14 },
- { type: "Comment", count: 4 },
- { type: "View", count: 113 },
- ],
- },
- {
- id: "14",
- title: "ํฅ์ ๋ณด๊ด๋ฒ ์๋ ค์ฃผ์ธ์",
- content: "ํฅ์๋ ์ด๋์ ๋ณด๊ดํ๋ ๊ฒ ๊ฐ์ฅ ์ข์๊ฐ์?",
- author: "๋ณด๊ด์",
- createdAt: "2025-04-12 14:20:00",
- categoryType: "QUESTION",
- meta: [
- { type: "Like", count: 3 },
- { type: "Comment", count: 3 },
- { type: "View", count: 27 },
- ],
- },
- {
- id: "15",
- title: "์์ ๊ฒ์ํ์์ ์ํตํด์",
- content: "์ค๋ ํ๋ฃจ ์์๋ ์ผ ์์ ๋กญ๊ฒ ์ด์ผ๊ธฐํด์.",
- author: "์ํต์",
- createdAt: "2025-04-11 13:45:00",
- categoryType: "FREEBOARD",
- meta: [
- { type: "Like", count: 8 },
- { type: "Comment", count: 1 },
- { type: "View", count: 54 },
- ],
- },
- {
- id: "16",
- title: "๋ด ํฅ์ ์ถ์ฒ ๋ฆฌ์คํธ",
- content: "๋ด์ ๋ฟ๋ฆฌ๊ธฐ ์ข์ ํฅ์ ๋ฆฌ์คํธ๋ฅผ ์ ๋ฆฌํด๋ดค์ด์.",
- author: "๋ดํฅ๊ธฐ",
- createdAt: "2025-04-10 11:20:00",
- categoryType: "RECOMMENDATION",
- meta: [
- { type: "Like", count: 10 },
- { type: "Comment", count: 2 },
- { type: "View", count: 98 },
- ],
- },
- {
- id: "17",
- title: "ํฅ์ ์ํ ์ด๋์ ๊ตฌํ๋์",
- content: "ํฅ์ ์ํ์ ์ฝ๊ฒ ๊ตฌํ ์ ์๋ ๊ณณ์ด ์์๊น์?",
- author: "์ํ๋ฌ",
- createdAt: "2025-04-09 10:30:00",
- categoryType: "QUESTION",
- meta: [
- { type: "Like", count: 6 },
- { type: "Comment", count: 2 },
- { type: "View", count: 39 },
- ],
- },
- {
- id: "18",
- title: "์์ ๋กญ๊ฒ ์๋ค ๋ ๋ ๊ณณ",
- content: "์์ ๋กญ๊ฒ ์ด์ผ๊ธฐ ๋๋๋ ๊ณต๊ฐ์
๋๋ค. ๋ชจ๋ ํ์ํด์!",
- author: "์๋ค์์ด",
- createdAt: "2025-04-08 18:45:00",
- categoryType: "FREEBOARD",
- meta: [
- { type: "Like", count: 7 },
- { type: "Comment", count: 0 },
- { type: "View", count: 29 },
- ],
- },
- {
- id: "19",
- title: "์ฌ๋ฆ์ ์ฐ๊ธฐ ์ข์ ํฅ์",
- content: "์ฌ๋ฆ์ฒ ์ ์์พํ๊ฒ ์ธ ์ ์๋ ํฅ์ ์ถ์ฒํด์.",
- author: "์ฌ๋ฆ๋จ",
- createdAt: "2025-04-07 09:12:00",
- categoryType: "RECOMMENDATION",
- meta: [
- { type: "Like", count: 13 },
- { type: "Comment", count: 5 },
- { type: "View", count: 121 },
- ],
- },
- {
- id: "20",
- title: "ํฅ์ ์๋ ๋ฅด๊ธฐ ์์๊น์?",
- content: "ํฅ์ ๋๋ฌธ์ ์๋ ๋ฅด๊ธฐ ๋ฐ์์ด ์๊ธธ ์ ์๋์?",
- author: "์๋ ๋ฅด๊ธฐ๋งจ",
- createdAt: "2025-04-06 14:30:00",
- categoryType: "QUESTION",
- meta: [
- { type: "Like", count: 2 },
- { type: "Comment", count: 1 },
- { type: "View", count: 22 },
- ],
- },
- {
- id: "21",
- title: "์ค๋์ ์์ ๊ฒ์ํ ์ด์ผ๊ธฐ",
- content: "์ค๋ ํ๋ฃจ๋ ๋ชจ๋ ํ๋ด์ธ์! ์์ ๊ฒ์ํ์์ ์ํตํด์.",
- author: "์ค๋๋์์ ",
- createdAt: "2025-04-05 08:55:00",
- categoryType: "FREEBOARD",
- meta: [
- { type: "Like", count: 5 },
- { type: "Comment", count: 0 },
- { type: "View", count: 31 },
- ],
- },
- {
- id: "22",
- title: "๊ฒจ์ธ์ ์ด์ธ๋ฆฌ๋ ํฅ์",
- content: "๊ฒจ์ธ์๋ ๋จธ์คํฌ๋ ์ฐ๋ ๊ณ์ด์ด ์ ์ด์ธ๋ฆฌ๋ ๊ฒ ๊ฐ์์.",
- author: "๊ฒจ์ธํฅ๊ธฐ",
- createdAt: "2025-04-04 17:40:00",
- categoryType: "RECOMMENDATION",
- meta: [
- { type: "Like", count: 12 },
- { type: "Comment", count: 3 },
- { type: "View", count: 108 },
- ],
- },
- {
- id: "23",
- title: "ํฅ์ ๋ ์ด์ด๋ง ๋ฐฉ๋ฒ",
- content: "๋ ๊ฐ์ง ์ด์์ ํฅ์๋ฅผ ์์ด ์ฐ๋ ๋ฐฉ๋ฒ์ด ๊ถ๊ธํฉ๋๋ค.",
- author: "๋ ์ด์ด๋ง์ด๋ณด",
- createdAt: "2025-04-03 13:15:00",
- categoryType: "QUESTION",
- meta: [
- { type: "Like", count: 4 },
- { type: "Comment", count: 1 },
- { type: "View", count: 34 },
- ],
- },
- {
- id: "24",
- title: "์์ ๊ฒ์ํ์์ ๋ง๋์",
- content: "์์ ๋กญ๊ฒ ๊ธ์ ๋จ๊ฒจ์ฃผ์ธ์. ๊ท์น๋ง ์ง์ผ์ฃผ์๋ฉด ๋ฉ๋๋ค!",
- author: "์ด์์",
- createdAt: "2025-04-02 10:10:00",
- categoryType: "FREEBOARD",
- meta: [
- { type: "Like", count: 6 },
- { type: "Comment", count: 1 },
- { type: "View", count: 41 },
- ],
- },
- {
- id: "25",
- title: "๊ฐ์ฑ๋น ์ข์ ํฅ์ ๋ชจ์",
- content: "๊ฐ๊ฒฉ๋๋น ๋ง์กฑ๋๊ฐ ๋์ ํฅ์ ๋ฆฌ์คํธ ๊ณต์ ํฉ๋๋ค.",
- author: "๊ฐ์ฑ๋นํผํธ",
- createdAt: "2025-04-01 19:30:00",
- categoryType: "RECOMMENDATION",
- meta: [
- { type: "Like", count: 11 },
- { type: "Comment", count: 3 },
- { type: "View", count: 115 },
- ],
- },
- {
- id: "26",
- title: "ํฅ์ ๊ณต๋ณ ์ธ์ฆํด์",
- content: "๋๋์ด ํฅ์ ๊ณต๋ณ์ด ๋์์ด์! ๋ค ์ฐ๊ณ ๋๋ ๋ฟ๋ฏํ๋ค์.",
- author: "๊ณต๋ณ๋ถ์",
- createdAt: "2025-03-31 08:55:00",
- categoryType: "FREEBOARD",
- meta: [
- { type: "Like", count: 3 },
- { type: "Comment", count: 0 },
- { type: "View", count: 21 },
- ],
- },
- {
- id: "27",
- title: "ํฅ์ ๊ณ ๋ฅด๋ ๊ธฐ์ค์?",
- content: "ํฅ์ ๊ณ ๋ฅผ ๋ ๊ฐ์ฅ ์ค์ํ๊ฒ ์๊ฐํ๋ ๊ธฐ์ค์ด ๋ญ๊ฐ์?",
- author: "์ ํ์ฅ์ ",
- createdAt: "2025-03-30 09:00:00",
- categoryType: "QUESTION",
- meta: [
- { type: "Like", count: 2 },
- { type: "Comment", count: 2 },
- { type: "View", count: 18 },
- ],
- },
- {
- id: "28",
- title: "๋จ์ ํฅ์ ์ถ์ฒ ๋ฆฌ์คํธ",
- content: "๋จ์๋ค์ด ๋ง์ด ์ฐ๋ ํฅ์ ๋ฆฌ์คํธ๋ฅผ ์ ๋ฆฌํด๋ดค์ด์.",
- author: "ํฅ์๋จ",
- createdAt: "2025-03-29 20:05:00",
- categoryType: "RECOMMENDATION",
- meta: [
- { type: "Like", count: 13 },
- { type: "Comment", count: 4 },
- { type: "View", count: 117 },
- ],
- },
- {
- id: "29",
- title: "ํฅ์ ๋ณด๊ด๋ฒ ๊ถ๊ธํด์",
- content: "ํฅ์๋ ์ด๋์ ๋ณด๊ดํ๋ ๊ฒ ๊ฐ์ฅ ์ข์๊ฐ์?",
- author: "๋ณด๊ด์",
- createdAt: "2025-03-28 14:20:00",
- categoryType: "QUESTION",
- meta: [
- { type: "Like", count: 2 },
- { type: "Comment", count: 1 },
- { type: "View", count: 16 },
- ],
- },
- {
- id: "30",
- title: "์์ ๊ฒ์ํ์์ ์ํตํด์",
- content: "์ค๋ ํ๋ฃจ ์์๋ ์ผ ์์ ๋กญ๊ฒ ์ด์ผ๊ธฐํด์.",
- author: "์ํต์",
- createdAt: "2025-03-27 13:45:00",
- categoryType: "FREEBOARD",
- meta: [
- { type: "Like", count: 7 },
- { type: "Comment", count: 2 },
- { type: "View", count: 36 },
- ],
- },
- {
- id: "31",
- title: "๋ด ํฅ์ ์ถ์ฒ ๋ฆฌ์คํธ",
- content: "๋ด์ ๋ฟ๋ฆฌ๊ธฐ ์ข์ ํฅ์ ๋ฆฌ์คํธ๋ฅผ ์ ๋ฆฌํด๋ดค์ด์.",
- author: "๋ดํฅ๊ธฐ",
- createdAt: "2025-03-26 11:20:00",
- categoryType: "RECOMMENDATION",
- meta: [
- { type: "Like", count: 12 },
- { type: "Comment", count: 3 },
- { type: "View", count: 102 },
- ],
- },
- {
- id: "32",
- title: "ํฅ์ ์ํ ์ด๋์ ๊ตฌํ๋์",
- content: "ํฅ์ ์ํ์ ์ฝ๊ฒ ๊ตฌํ ์ ์๋ ๊ณณ์ด ์์๊น์?",
- author: "์ํ๋ฌ",
- createdAt: "2025-03-25 10:30:00",
- categoryType: "QUESTION",
- meta: [
- { type: "Like", count: 4 },
- { type: "Comment", count: 1 },
- { type: "View", count: 20 },
- ],
- },
- {
- id: "33",
- title: "์์ ๋กญ๊ฒ ์๋ค ๋ ๋ ๊ณณ",
- content: "์์ ๋กญ๊ฒ ์ด์ผ๊ธฐ ๋๋๋ ๊ณต๊ฐ์
๋๋ค. ๋ชจ๋ ํ์ํด์!",
- author: "์๋ค์์ด",
- createdAt: "2025-03-24 18:45:00",
- categoryType: "FREEBOARD",
- meta: [
- { type: "Like", count: 6 },
- { type: "Comment", count: 1 },
- { type: "View", count: 27 },
- ],
- },
- {
- id: "34",
- title: "์ฌ๋ฆ์ ์ฐ๊ธฐ ์ข์ ํฅ์",
- content: "์ฌ๋ฆ์ฒ ์ ์์พํ๊ฒ ์ธ ์ ์๋ ํฅ์ ์ถ์ฒํด์.",
- author: "์ฌ๋ฆ๋จ",
- createdAt: "2025-03-23 09:12:00",
- categoryType: "RECOMMENDATION",
- meta: [
- { type: "Like", count: 11 },
- { type: "Comment", count: 2 },
- { type: "View", count: 99 },
- ],
- },
- {
- id: "35",
- title: "ํฅ์ ์๋ ๋ฅด๊ธฐ ์์๊น์?",
- content: "ํฅ์ ๋๋ฌธ์ ์๋ ๋ฅด๊ธฐ ๋ฐ์์ด ์๊ธธ ์ ์๋์?",
- author: "์๋ ๋ฅด๊ธฐ๋งจ",
- createdAt: "2025-03-22 14:30:00",
- categoryType: "QUESTION",
- meta: [
- { type: "Like", count: 1 },
- { type: "Comment", count: 0 },
- { type: "View", count: 14 },
- ],
- },
- {
- id: "36",
- title: "์ค๋์ ์์ ๊ฒ์ํ ์ด์ผ๊ธฐ",
- content: "์ค๋ ํ๋ฃจ๋ ๋ชจ๋ ํ๋ด์ธ์! ์์ ๊ฒ์ํ์์ ์ํตํด์.",
- author: "์ค๋๋์์ ",
- createdAt: "2025-03-21 08:55:00",
- categoryType: "FREEBOARD",
- meta: [
- { type: "Like", count: 4 },
- { type: "Comment", count: 1 },
- { type: "View", count: 19 },
- ],
- },
-];
diff --git a/src/lib/mocks/communityPosts.ts b/src/lib/mocks/communityPosts.ts
deleted file mode 100644
index 45bb21d4..00000000
--- a/src/lib/mocks/communityPosts.ts
+++ /dev/null
@@ -1,58 +0,0 @@
-export interface CommunityPost {
- id: number;
- title: string;
- commentCount: number;
-}
-
-// ์ง๋ฌธ๊ฒ์ํ ๋๋ฏธ ๋ฐ์ดํฐ
-export const mockQuestionPosts: CommunityPost[] = [
- { id: 1, title: "์ ๋น์ ๋ฒ๋ฅ ์ด ์ ํ", commentCount: 10003 },
- {
- id: 2,
- title: "์ ๋น์ ๋ฒ๋ฅ ์ด ์ ํ๋ ๋ฐ์ ์ํ์ฌ ๊ตญ๊ฐ์ ๋ณดํธ๋ฅผ...",
- commentCount: 999,
- },
- { id: 3, title: "์ ๋น์ ๋ฒ๋ฅ ์ด ์ ํ๋ ๋ฐ์ ์ํ์ฌ", commentCount: 999 },
- { id: 4, title: "์ ๋น์ ๋ฒ๋ฅ ์ด ์ ", commentCount: 999 },
- { id: 5, title: "์ ๋น์ ๋ฒ๋ฅ ์ด ์ ํ๋ ๋ฐ์ ์", commentCount: 999 },
- {
- id: 6,
- title: "์ ๋น์ ๋ฒ๋ฅ ์ด ์ ํ๋ ๋ฐ์ ์ํ์ฌ ๊ตญ๊ฐ์ ๋ณดํธ๋ฅผ...",
- commentCount: 999,
- },
- { id: 7, title: "์ ๋น์ ๋ฒ๋ฅ ์ด ์ ํ๋ ๋ฐ", commentCount: 999 },
- { id: 8, title: "์ ๋น์ ๋ฒ๋ฅ ์ด ์ ํ๋ ๋ฐ", commentCount: 999 },
- { id: 9, title: "์ ๋น์ ๋ฒ๋ฅ ์ด ์ ํ๋ ๋ฐ", commentCount: 999 },
-];
-
-// ์ถ์ฒ๊ฒ์ํ ๋๋ฏธ ๋ฐ์ดํฐ
-export const mockRecommendPosts: CommunityPost[] = [
- { id: 1, title: "ํฅ์ ์ถ์ฒ ๋ถํ๋๋ ค์!", commentCount: 120 },
- { id: 2, title: "์ฌ๋ฆ์ ์ด์ธ๋ฆฌ๋ ์์ํ ํฅ์ ์์๊น์?", commentCount: 87 },
- { id: 3, title: "๊ฐ์ฑ๋น ์ข์ ๋ฐ์ผ๋ฆฌ ํฅ์ ์ถ์ฒ", commentCount: 64 },
- { id: 4, title: "20๋ ๋จ์ ํฅ์ ์ถ์ฒํด์ฃผ์ธ์", commentCount: 45 },
- { id: 5, title: "์คํผ์ค์ฉ ๋ฌด๋ํ ํฅ์ ์ถ์ฒ", commentCount: 33 },
- { id: 6, title: "์ฌ์น ์ ๋ฌผ์ฉ ํฅ์ ์ถ์ฒ๋ฐ์์", commentCount: 27 },
- { id: 7, title: "๊ฐ์์ ์ฐ๊ธฐ ์ข์ ํฅ์ ์ถ์ฒ", commentCount: 21 },
- { id: 8, title: "์ง์๋ ฅ ์ข์ ํฅ์ ์ถ์ฒ", commentCount: 19 },
- { id: 9, title: "ํ๋ก๋ด ๊ณ์ด ํฅ์ ์ถ์ฒํด ์ฃผ์ธ์", commentCount: 15 },
-];
-
-// ์์ ๊ฒ์ํ ๋๋ฏธ ๋ฐ์ดํฐ
-export const mockFreePosts: CommunityPost[] = [
- { id: 1, title: "์ค๋ ์ฐ ํฅ์ ์ธ๋ฐ์ฑ ํ๊ธฐ", commentCount: 43 },
- { id: 2, title: "ํฅ์ ์ํ ๋๋ํฉ๋๋ค!", commentCount: 38 },
- { id: 3, title: "ํฅ์ ๋ฟ๋ฆฌ๋ ๊ฟํ ๊ณต์ ํด์", commentCount: 29 },
- { id: 4, title: "ํฅ์๋ณ ์์๊ฒ ๋ณด๊ดํ๋ ๋ฒ", commentCount: 25 },
- { id: 5, title: "ํฅ์ ๊ณต๋ณ ์ธ์ฆํฉ๋๋ค", commentCount: 18 },
- { id: 6, title: "์ค๋ ๋ ์จ๋ ์ด์ธ๋ฆฌ๋ ํฅ์ ์ถ์ฒ", commentCount: 15 },
- { id: 7, title: "ํฅ์ ๊ด๋ จ ์ ํ๋ธ ์ถ์ฒํด์", commentCount: 12 },
- { id: 8, title: "ํฅ์ ์ํฅ ํ๊ธฐ ๋จ๊น๋๋ค", commentCount: 10 },
- { id: 9, title: "์ค๋์ TMI: ํฅ์ ๋ฟ๋ฆฌ๋ค ์์์ด์ใ
ใ
", commentCount: 7 },
-];
-
-export const boardDataMap: { [key: string]: CommunityPost[] } = {
- QUESTION: mockQuestionPosts,
- RECOMMENDATION: mockRecommendPosts,
- FREEBOARD: mockFreePosts,
-};
diff --git a/src/lib/mocks/fetchUser.ts b/src/lib/mocks/fetchUser.ts
deleted file mode 100644
index 0cb78134..00000000
--- a/src/lib/mocks/fetchUser.ts
+++ /dev/null
@@ -1,342 +0,0 @@
-// import { ActivityData } from "@/components/domains/user/sections/sections.type";
-// import { ApiReviewResponse } from "../hono/schemas/review.schema";
-// import { ApiPostResponse } from "../hono/schemas/community.schema";
-// import { CommentResponse } from "../hono/schemas/comment.schema";
-// import { PerfumeBookmark } from "@zod/modelSchema/PerfumeBookmarkSchema";
-
-// export interface MockCollectionItem {
-// id: string | number;
-// imageUrl: string;
-// title: string;
-// }
-
-// export interface MockPerfumeBookmark {
-// id: number;
-// name: string;
-// }
-
-// export interface MockCommunityBookmark {
-// id: number;
-// title: string;
-// category: string;
-// }
-
-// export interface MockPerfumeBookmarksData {
-// perfumes: MockPerfumeBookmark[];
-// }
-
-// export interface MockCommunityBookmarksData {
-// community: MockCommunityBookmark[];
-// }
-
-// export interface MockMyReview {
-// id: number;
-// perfume: string;
-// content: string;
-// chips: string[];
-// isAuthor: boolean;
-// isMyPage: boolean;
-// }
-
-// export interface MockMyPost {
-// id: number;
-// title: string;
-// content: string;
-// isAuthor: boolean;
-// }
-
-// export interface MockMyComment {
-// id: number;
-// postId: number;
-// content: string;
-// }
-
-// export interface MockLikedPerfume {
-// id: number;
-// name: string;
-// }
-
-// export interface MockLikedPost {
-// id: number;
-// title: string;
-// category: string;
-// }
-
-// export interface MockActivityData {
-// mockReviews: ApiReviewResponse[];
-// mockPosts: ApiPostResponse[];
-// mockComments: CommentResponse[];
-// likedPerfumes: PerfumeBookmark[];
-// likedPosts: ApiPostResponse[];
-// }
-
-// export interface MockProfileData {
-// id: string;
-// name: string;
-// nickname: string;
-// level?: number;
-// avatarUrl?: string;
-// gender: string;
-// age: number;
-// postCount?: number;
-// followerCount?: number;
-// followingCount?: number;
-// }
-
-// export type UserPageTapData =
-// | MockCollectionItem[]
-// | MockPerfumeBookmarksData
-// | ActivityData
-// | MockProfileData;
-
-// export interface TabData {
-// tap: string;
-// data: UserPageTapData | null;
-// error?: string;
-// }
-
-// const generateMockCollection = (count: number): MockCollectionItem[] => {
-// return Array.from({ length: count }, (_, i) => ({
-// id: `col-item-${i + 1}`,
-// imageUrl: `https://picsum.photos/seed/col${i}/200/300`,
-// title: `์ปฌ๋ ์
์์ดํ
${i + 1}`,
-// }));
-// };
-
-// export async function fetchMockCollectionData(
-// userId: string
-// ): Promise {
-// console.log(`Fetching collection : ${userId}`);
-// await new Promise((resolve) => setTimeout(resolve, 100));
-// return generateMockCollection(12);
-// }
-
-// export async function fetchMockPerfumeBookmarksData(
-// userId: string
-// ): Promise {
-// console.log(`Fetching bookmarks : ${userId}`);
-// await new Promise((resolve) => setTimeout(resolve, 100));
-// return [
-// { id: 3, name: "๋ถ๋งํฌ ํฅ์ 1" },
-// { id: 4, name: "๋ถ๋งํฌ ํฅ์ 2" },
-// ];
-// }
-
-// export async function fetchMockCommunityBookmarks(
-// userId: string
-// ): Promise {
-// console.log(`Fetching community bookmarks : ${userId}`);
-// await new Promise((resolve) => setTimeout(resolve, 100));
-// return [
-// { id: 5, title: "ํฅ์ ์ ๋ณด ๊ณต์ ", category: "์์ ๊ฒ์ํ" },
-// { id: 6, title: "๋ฆฌ๋ทฐ ๋ชจ์", category: "๋ฆฌ๋ทฐ ๊ฒ์ํ" },
-// ];
-// }
-
-// export async function fetchMockActivityData(
-// userId: string
-// ): Promise {
-// console.log(`Fetching activity : ${userId}`);
-// await new Promise((resolve) => setTimeout(resolve, 100));
-
-// return {
-// // ๋ฆฌ๋ทฐ ๋ชฉ๋ฐ์ดํฐ
-// mockReviews: [
-// {
-// id: "review-1",
-// usageStatus: "CURRENTLY_USING",
-// content: "์ ๋ง ์ข์ ํฅ์์
๋๋ค. ์ง์๋ ฅ๋ ์ข๊ณ ํฅ๋ ๋ง์์ ๋ค์ด์.",
-// createdAt: new Date("2024-01-15T10:30:00.000Z"),
-// author: {
-// id: "user-1",
-// nickname: "ํฅ์๋ฌ๋ฒ",
-// imageUrl: "https://example.com/avatar1.jpg",
-// },
-// chips: {
-// feeling: "GOOD",
-// longevity: "LONG_LASTING",
-// sillage: "MODERATE",
-// genderTone: "UNISEX",
-// season: ["SPRING", "SUMMER"],
-// timeOfDay: "DAY",
-// pricePerception: "REASONABLE",
-// },
-// },
-// {
-// id: "review-2",
-// usageStatus: "USED_BEFORE",
-// content:
-// "์ํ๋ก ์จ๋ดค๋๋ฐ ์๊ฐ๋ณด๋ค ๊ด์ฐฎ๋ค์. ๋ค์์ ์ ํ ๊ตฌ๋งค ๊ณ ๋ ค์ค์
๋๋ค.",
-// createdAt: new Date("2024-01-20T15:45:00.000Z"),
-// author: {
-// id: "user-2",
-// nickname: "์ผํธ๋ง๋์",
-// imageUrl: "https://example.com/avatar2.jpg",
-// },
-// chips: {
-// feeling: "BEST",
-// longevity: "LONG_LASTING",
-// sillage: "STRONG",
-// genderTone: "FEMININE",
-// season: ["AUTUMN"],
-// timeOfDay: "NIGHT",
-// pricePerception: "EXPENSIVE",
-// },
-// },
-// ],
-
-// // ๊ฒ์๊ธ ๋ชฉ๋ฐ์ดํฐ
-// mockPosts: [
-// {
-// id: "post-1",
-// title: "๋ด์ ์ด์ธ๋ฆฌ๋ ํฅ์ ์ถ์ฒํด์ฃผ์ธ์",
-// content:
-// "๋ ์จ๊ฐ ๋ฐ๋ปํด์ง๋ฉด์ ๊ฐ๋ฒผ์ด ํฅ์๋ฅผ ์ฐพ๊ณ ์์ด์. ํ๋ก๋ด์ด๋ ์ํธ๋ฌ์ค ๊ณ์ด๋ก ์ถ์ฒ ๋ถํ๋๋ฆฝ๋๋ค.",
-// contentText:
-// "๋ ์จ๊ฐ ๋ฐ๋ปํด์ง๋ฉด์ ๊ฐ๋ฒผ์ด ํฅ์๋ฅผ ์ฐพ๊ณ ์์ด์. ํ๋ก๋ด์ด๋ ์ํธ๋ฌ์ค ๊ณ์ด๋ก ์ถ์ฒ ๋ถํ๋๋ฆฝ๋๋ค.",
-// category: "RECOMMENDATION",
-// viewCount: 127,
-// likeCount: 15,
-// commentCount: 8,
-// thumbnailUrl: "https://example.com/post1-thumb.jpg",
-// createdAt: "2024-01-18T09:15:00.000Z",
-// updatedAt: null,
-// author: {
-// id: "user-1",
-// nickname: "ํฅ์๋ฌ๋ฒ",
-// imageUrl: "https://example.com/avatar1.jpg",
-// },
-// },
-// {
-// id: "post-2",
-// title: "ํฅ์ ์ง์๋ ฅ ๋๋ฆฌ๋ ํ ๊ณต์ ",
-// content:
-// "ํฅ์๋ฅผ ์ค๋ ์ง์์ํค๋ ๋ฐฉ๋ฒ๋ค์ ์ ๋ฆฌํด๋ดค์ด์. ๋์์ด ๋์์ผ๋ฉด ์ข๊ฒ ์ต๋๋ค.",
-// contentText:
-// "ํฅ์๋ฅผ ์ค๋ ์ง์์ํค๋ ๋ฐฉ๋ฒ๋ค์ ์ ๋ฆฌํด๋ดค์ด์. ๋์์ด ๋์์ผ๋ฉด ์ข๊ฒ ์ต๋๋ค.",
-// category: "FREEBOARD",
-// viewCount: 89,
-// likeCount: 22,
-// commentCount: 12,
-// thumbnailUrl: null,
-// createdAt: "2024-01-22T14:20:00.000Z",
-// updatedAt: "2024-01-22T14:25:00.000Z",
-// author: {
-// id: "user-2",
-// nickname: "์ผํธ๋ง๋์",
-// imageUrl: "https://example.com/avatar2.jpg",
-// },
-// },
-// ],
-
-// // ๋๊ธ ๋ชฉ๋ฐ์ดํฐ
-// mockComments: [
-// {
-// id: "comment-1",
-// content: "์ ๋ ๊ฐ์ ๊ณ ๋ฏผ์ด์๋๋ฐ ์ ๋ง ์ ์ฉํ ์ ๋ณด๋ค์. ๊ฐ์ฌํฉ๋๋ค!",
-// parentId: null,
-// postId: "post-2",
-// published: true,
-// createdAt: "2024-01-22T16:30:00.000Z",
-// updatedAt: "2024-01-22T16:30:00.000Z",
-// author: {
-// id: "user-3",
-// nickname: "๋ทฐํฐ๋ํ",
-// imageUrl: "https://example.com/avatar3.jpg",
-// },
-// replies: [
-// {
-// id: "reply-1",
-// content: "๋์์ด ๋์๋ค๋ ๊ธฐ์๋ค์!",
-// parentId: "comment-1",
-// postId: "post-2",
-// published: true,
-// createdAt: "2024-01-22T17:00:00.000Z",
-// updatedAt: "2024-01-22T17:00:00.000Z",
-// author: {
-// id: "user-2",
-// nickname: "์ผํธ๋ง๋์",
-// imageUrl: "https://example.com/avatar2.jpg",
-// },
-// },
-// ],
-// },
-// {
-// id: "comment-2",
-// content: "๋ดํฅ์๋ก๋ ์กฐ๋ง๋ก ํผ์ค๋ ์ค ๋ธ๋ฌ์ ์ค์จ์ด๋ ์ถ์ฒ๋๋ ค์!",
-// parentId: null,
-// postId: "post-1",
-// published: true,
-// createdAt: "2024-01-18T11:45:00.000Z",
-// updatedAt: "2024-01-18T11:45:00.000Z",
-// author: {
-// id: "user-4",
-// nickname: "ํฅ์์ปฌ๋ ํฐ",
-// imageUrl: "https://example.com/avatar4.jpg",
-// },
-// replies: [],
-// },
-// ],
-
-// // ํฅ์ ๋ถ๋งํฌ ๋ชฉ๋ฐ์ดํฐ
-// likedPerfumes: [
-// {
-// id: "bookmark-1",
-// userId: "user-1",
-// perfumeId: "perfume-1",
-// createdAt: new Date("2024-01-10T08:00:00.000Z"),
-// isPublic: true,
-// },
-// {
-// id: "bookmark-2",
-// userId: "user-1",
-// perfumeId: "perfume-2",
-// createdAt: new Date("2024-01-16T12:30:00.000Z"),
-// isPublic: false,
-// },
-// ],
-
-// // ์ข์์ํ ๊ฒ์๊ธ ๋ชฉ๋ฐ์ดํฐ
-// likedPosts: [
-// {
-// id: "liked-post-1",
-// title: "ํฅ์ ์
๋ฌธ์๋ฅผ ์ํ ๊ฐ์ด๋",
-// content: "ํฅ์๋ฅผ ์ฒ์ ์์ํ๋ ๋ถ๋ค์ ์ํ ๊ธฐ๋ณธ ์ ๋ณด๋ค์ ๋ชจ์๋ดค์ต๋๋ค.",
-// contentText:
-// "ํฅ์๋ฅผ ์ฒ์ ์์ํ๋ ๋ถ๋ค์ ์ํ ๊ธฐ๋ณธ ์ ๋ณด๋ค์ ๋ชจ์๋ดค์ต๋๋ค.",
-// category: "QUESTION",
-// viewCount: 203,
-// likeCount: 45,
-// commentCount: 18,
-// thumbnailUrl: "https://example.com/guide-thumb.jpg",
-// createdAt: "2024-01-12T13:20:00.000Z",
-// updatedAt: null,
-// author: {
-// id: "user-5",
-// nickname: "ํฅ์์ ๋ฌธ๊ฐ",
-// imageUrl: "https://example.com/avatar5.jpg",
-// },
-// },
-// {
-// id: "liked-post-2",
-// title: "๊ฒจ์ธ์ฒ ์ถ์ฒ ํฅ์ ๋ฒ ์คํธ 5",
-// content:
-// "์ถ์ด ๊ฒจ์ธ์ ์ด์ธ๋ฆฌ๋ ๋ฐ๋ปํ๊ณ ํฌ๊ทผํ ํฅ์๋ค์ ์ถ์ฒํด๋๋ฆฝ๋๋ค.",
-// contentText:
-// "์ถ์ด ๊ฒจ์ธ์ ์ด์ธ๋ฆฌ๋ ๋ฐ๋ปํ๊ณ ํฌ๊ทผํ ํฅ์๋ค์ ์ถ์ฒํด๋๋ฆฝ๋๋ค.",
-// category: "RECOMMENDATION",
-// viewCount: 156,
-// likeCount: 31,
-// commentCount: 9,
-// thumbnailUrl: null,
-// createdAt: "2024-01-08T19:45:00.000Z",
-// updatedAt: "2024-01-09T10:15:00.000Z",
-// author: {
-// id: "user-6",
-// nickname: "์ํฐ์ผํธ",
-// imageUrl: "https://example.com/avatar6.jpg",
-// },
-// },
-// ],
-// };
-// }
diff --git a/src/lib/mocks/perfumeCard.ts b/src/lib/mocks/perfumeCard.ts
deleted file mode 100644
index 4bb74f88..00000000
--- a/src/lib/mocks/perfumeCard.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-export const mockPerfumeCardData = {
- brandName: "Brand Name Brand Name Brand Name Brand Name",
- perfumeName: "Perfume Name Perfume Name Perfume Name ",
- perfumeImage: "https://picsum.photos/180",
-};
diff --git a/src/lib/mocks/postCard.ts b/src/lib/mocks/postCard.ts
deleted file mode 100644
index a9b46584..00000000
--- a/src/lib/mocks/postCard.ts
+++ /dev/null
@@ -1,30 +0,0 @@
-// import { PostCardProps } from "@/components/commons/card/postCard/postCard.types";
-
-// export const mockPostCardData: PostCardProps = {
-// post: {
-// id: "7e314065-2f18-49b2-a55d-9b168d9cfe52",
-// title: "ํ๋ ๋ฌด๋ฏผ",
-// content:
-// '
',
-// userId: "dc9bb3fd-48c1-4044-8038-6141c00d1ff9",
-// category: "QUESTION",
-// published: true,
-// viewCount: 5,
-// likeCount: 0,
-// commentCount: 1,
-// thumbnailUrl:
-// "https://wvedpvxspndgyoisudyr.supabase.co/storage/v1/object/public/post-image/1758279654182____________.webp",
-// createdAt: new Date("2025-09-19T11:00:54.567Z"),
-// updatedAt: new Date("2025-09-25T08:29:58.828Z"),
-// contentText: "",
-// },
-// author: {
-// id: "dc9bb3fd-48c1-4044-8038-6141c00d1ff9",
-// nickname: "๊นํ์",
-// imageUrl:
-// "https://lh3.googleusercontent.com/a/ACg8ocLKak4Z58c-sEvRicBiir32K0nOsF4oA8nMp156tKR8eid5ZA=s96-c",
-// },
-// createdAt: new Date("2025-09-19T11:00:54.567Z"),
-// isAuthor: false,
-// cardType: "small",
-// };
diff --git a/src/lib/mocks/reply.ts b/src/lib/mocks/reply.ts
deleted file mode 100644
index 47d69fe7..00000000
--- a/src/lib/mocks/reply.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-export const mockReplyData = {
- content: "์ ๋ ์ฐธ์ฌ ๊ฐ๋ฅํ ๊น์? ๋๊ธ ๋จ๊ธธํ
๋ ์ฐ๋ฝ์ฃผ์ธ์",
- postId: "1",
- createdAt: "2025-01-28T00:00:00.000Z",
-};
diff --git a/src/lib/mocks/reviewCard.ts b/src/lib/mocks/reviewCard.ts
deleted file mode 100644
index 172afc9e..00000000
--- a/src/lib/mocks/reviewCard.ts
+++ /dev/null
@@ -1,57 +0,0 @@
-// oxlint-disable-next-line no-empty-file
-// import { REVIEW_STATUSES } from "@/components/commons/author/author.constants";
-// import { ReviewCardProps } from "@/components/commons/card/reviewCard/reviewCard.types";
-
-// export const mockReviewCardData: ReviewCardProps = {
-// brand: "๋ฅด ๋ผ๋ณด",
-// title: "๋ผ ๋ง์ฐจ 26 EDP",
-// review:
-// "์์พํ๋ฉด์๋ ๊น์ ์ฐ๋ ํฅ์ด ์ธ์์ ์ด์์. ์ํฅ์ด ์ค๋ ๋จ์์ ํ๋ฃจ ์ข
์ผ ๊ธฐ๋ถ์ด ์ข์์ง๋๋ค. ๋ฐ์ผ๋ฆฌ๋ก ์ฐ๊ธฐ์๋ ๋ถ๋ด ์๊ณ , ํน๋ณํ ๋ ์๋ ์ ์ด์ธ๋ ค์!",
-// createdAt: new Date("2024-04-20"),
-// info: {
-// type: "review",
-// item: {
-// status: REVIEW_STATUSES.NOW, // "์ง๊ธ ์ฐ๊ณ ์์ด์"
-// },
-// },
-// chips: ["์์พํจ", "์ฐ๋", "์ง์๋ ฅ์ข์", "๋ฐ์ผ๋ฆฌ", "ํน๋ณํ ๋ "],
-// imageUrl:
-// "https://wvedpvxspndgyoisudyr.supabase.co/storage/v1/object/public/perfume_image/perfumes/375x500.31172.jpg",
-// isMyPage: true,
-// isAuthor: true,
-// author: {
-// id: "mock-author-id",
-// nickname: "์์ฑ์",
-// imageUrl: "",
-// },
-// };
-
-// export const mockMainpageReviewCardData: ReviewCardProps = {
-// brand: "๋ฅด ๋ผ๋ณด",
-// title: "๋ผ ๋ง์ฐจ 26 EDP",
-// review:
-// "์์พํ๋ฉด์๋ ๊น์ ์ฐ๋ ํฅ์ด ์ธ์์ ์ด์์. ์ํฅ์ด ์ค๋ ๋จ์์ ํ๋ฃจ ์ข
์ผ ๊ธฐ๋ถ์ด ์ข์์ง๋๋ค. ๋ฐ์ผ๋ฆฌ๋ก ์ฐ๊ธฐ์๋ ๋ถ๋ด ์๊ณ , ํน๋ณํ ๋ ์๋ ์ ์ด์ธ๋ ค์!",
-// createdAt: new Date("2024-04-20"),
-// info: {
-// type: "review",
-// item: {
-// status: REVIEW_STATUSES.NOW, // "์ง๊ธ ์ฐ๊ณ ์์ด์"
-// },
-// },
-// chips: [
-// "๐ ์ต๊ณ ์์!",
-// "โ๏ธ ๊ธด ์ง์๋ ฅ (6์๊ฐ ์ด์)",
-// "๐ ๊ฐ์",
-// "๋ฐ์ผ๋ฆฌ",
-// "ํน๋ณํ ๋ ",
-// ],
-// imageUrl:
-// "https://wvedpvxspndgyoisudyr.supabase.co/storage/v1/object/public/perfume_image/perfumes/375x500.31172.jpg",
-// isMyPage: false,
-// isAuthor: true,
-// author: {
-// id: "mock-author-id",
-// nickname: "์์ฑ์",
-// imageUrl: "",
-// },
-// };
diff --git a/src/lib/stores/createRecentItemsStore.ts b/src/lib/stores/createRecentItemsStore.ts
deleted file mode 100644
index 8a00f86d..00000000
--- a/src/lib/stores/createRecentItemsStore.ts
+++ /dev/null
@@ -1,82 +0,0 @@
-import { create } from "zustand";
-import { persist, createJSONStorage } from "zustand/middleware";
-
-// ์คํ ์ด์ ์ ์ฅ๋ ์์ดํ
์ ๊ธฐ๋ณธ ํํ ์ ์
-export type GenericRecentItem = {
- id: string;
- type: string; // 'perfume', 'post' ๋ฑ ์์ดํ
ํ์
์ ๋ช
์
- item: T;
- viewedAt: number;
-};
-
-// ์คํ ์ด์ ์ํ(State) ํ์
์ ์
-export interface RecentItemsState {
- items: GenericRecentItem[];
- lastSyncedAt: number | null;
- _hasHydrated: boolean;
- setLastSyncedAt: (timestamp: number) => void;
- addItem: (newItem: { id: string; item: T }) => void;
- setItems: (items: GenericRecentItem[]) => void;
- clearItems: () => void;
-}
-
-// ์คํ ์ด ์์ฑ์ ์ํ ์ค์ ๊ฐ ํ์
์ ์
-interface CreateRecentItemsStoreOptions {
- name: string; // localStorage์ ์ ์ฅ๋ ํค ์ด๋ฆ
- maxItems: number; // ์ต๋ ์ ์ฅ ๊ฐ์
- type: string; // ์์ดํ
ํ์
-}
-
-/**
- * ์ต๊ทผ ๋ณธ ์์ดํ
๋ชฉ๋ก์ ๊ด๋ฆฌํ๋ Zustand ์คํ ์ด๋ฅผ ์์ฑํ๋ ํฉํ ๋ฆฌ ํจ์์
๋๋ค.
- * @param options ์คํ ์ด ์ค์ (name, maxItems, type)
- * @returns Zustand ์คํ ์ด Hook
- */
-export function createRecentItemsStore({
- name,
- maxItems,
- type,
-}: CreateRecentItemsStoreOptions) {
- return create>()(
- persist(
- (set, get) => ({
- items: [],
- lastSyncedAt: null,
- _hasHydrated: false,
- setLastSyncedAt: (timestamp: number) =>
- set({ lastSyncedAt: timestamp }),
- addItem: (newItem) => {
- const currentItems = get().items;
-
- const filteredItems = currentItems.filter(
- (item) => item.id !== newItem.id
- );
-
- const newItemWithTimestamp: GenericRecentItem = {
- ...newItem,
- type,
- viewedAt: Date.now(),
- };
-
- const updatedItems = [newItemWithTimestamp, ...filteredItems];
- const slicedItems = updatedItems.slice(0, maxItems);
-
- set({ items: slicedItems });
- },
- setItems: (items) => {
- set({ items });
- },
- clearItems: () => set({ items: [], lastSyncedAt: null }),
- }),
- {
- name,
- storage: createJSONStorage(() => localStorage),
- onRehydrateStorage: () => (state) => {
- if (state) {
- state._hasHydrated = true;
- }
- },
- }
- )
- );
-}
diff --git a/src/lib/stores/useCountStore.ts b/src/lib/stores/useCountStore.ts
deleted file mode 100644
index 1255f061..00000000
--- a/src/lib/stores/useCountStore.ts
+++ /dev/null
@@ -1,11 +0,0 @@
-import { create } from "zustand";
-
-interface TotalStore {
- totalCount: number | null;
- setTotalCount: (n: number) => void;
-}
-
-export const useTotalStore = create((set) => ({
- totalCount: null,
- setTotalCount: (n: number) => set({ totalCount: n }),
-}));
diff --git a/src/lib/stores/useFilterStore.ts b/src/lib/stores/useFilterStore.ts
deleted file mode 100644
index 84756b40..00000000
--- a/src/lib/stores/useFilterStore.ts
+++ /dev/null
@@ -1,131 +0,0 @@
-import { create } from "zustand";
-
-interface FilterStore {
- committedFilters: Record;
- pendingFilters: Record;
-
- handlePendingChange: (category: string, value: string) => void;
- resetPending: () => void;
- commitFilters: (category: string) => void;
- cancelPending: (category: string) => void;
- initializePending: () => void;
-
- // ๊ฐ๋ณ ํํฐ ํญ๋ชฉ ์ ๊ฑฐ (FilterList์์ ์ฌ์ฉ)
- closeFilter: (id: string) => void;
- // ์ ์ฒด ํํฐ ์ด๊ธฐํ
- resetFilters: () => void;
-
- // ๋ ๊ฑฐ์ ํธํ์ฑ (๊ธฐ์กด ์ฝ๋์์ ํธํ)
- filters: Record;
-}
-
-export const useFilterStore = create((set, get) => ({
- committedFilters: {},
- pendingFilters: {},
-
- // Pending ํํฐ ๋ณ๊ฒฝ (ํ ๊ธ)
- handlePendingChange: (category, value) =>
- set((state) => {
- const newPending = { ...state.pendingFilters };
- const prevArray = newPending[category] ?? [];
-
- const nextArray = prevArray.includes(value)
- ? prevArray.filter((v) => v !== value)
- : [...prevArray, value].sort();
-
- if (nextArray.length === 0) {
- delete newPending[category];
- } else {
- newPending[category] = nextArray;
- }
-
- return { pendingFilters: newPending };
- }),
-
- // Pending ์ด๊ธฐํ (๋ชจ๋ฌ ๋ด "์ด๊ธฐํ" ๋ฒํผ)
- resetPending: () =>
- set(() => ({
- pendingFilters: {},
- })),
-
- // Pending โ Committed ์ ์ฉ (๋ชจ๋ฌ ์ ์ถ)
- commitFilters: (category) =>
- set((state) => {
- const newCommitted = { ...state.committedFilters };
- const pendingValue = state.pendingFilters[category];
-
- if (pendingValue && pendingValue.length > 0) {
- newCommitted[category] = pendingValue;
- } else {
- delete newCommitted[category];
- }
-
- return {
- committedFilters: newCommitted,
- };
- }),
-
- // ๋ชจ๋ฌ ์ทจ์ ์ Pending ๋ณต์ (๋ชจ๋ฌ ์ด ๋ committed ์ํ๋ก ๋๊ธฐํ)
- cancelPending: (category) =>
- set((state) => {
- const newPending = { ...state.pendingFilters };
- const committedValue = state.committedFilters[category];
-
- if (committedValue && committedValue.length > 0) {
- newPending[category] = [...committedValue]; // ๋ณต์ฌ๋ณธ ์์ฑ
- } else {
- delete newPending[category];
- }
-
- return { pendingFilters: newPending };
- }),
-
- // ๋ชจ๋ฌ ์ด ๋ ์ ์ฒด pending์ committed๋ก ๋๊ธฐํ
- initializePending: () =>
- set((state) => ({
- pendingFilters: JSON.parse(JSON.stringify(state.committedFilters)),
- })),
-
- // ๊ฐ๋ณ ํํฐ ์ ๊ฑฐ (FilterList์ X ๋ฒํผ)
- closeFilter: (id) =>
- set((state) => {
- const newCommitted = { ...state.committedFilters };
- for (const category in newCommitted) {
- const values = newCommitted[category];
- if (values.includes(id)) {
- const nextArray = values.filter((v) => v !== id);
- if (nextArray.length === 0) {
- delete newCommitted[category];
- } else {
- newCommitted[category] = nextArray;
- }
- }
- }
- // pending๋ ๋๊ธฐํ
- const newPending = { ...state.pendingFilters };
- for (const category in newPending) {
- const values = newPending[category];
- if (values.includes(id)) {
- const nextArray = values.filter((v) => v !== id);
- if (nextArray.length === 0) {
- delete newPending[category];
- } else {
- newPending[category] = nextArray;
- }
- }
- }
- return { committedFilters: newCommitted, pendingFilters: newPending };
- }),
-
- // ์ ์ฒด ํํฐ ์ด๊ธฐํ
- resetFilters: () =>
- set({
- committedFilters: {},
- pendingFilters: {},
- }),
-
- // ๋ ๊ฑฐ์ ํธํ์ฑ
- get filters() {
- return get().committedFilters;
- },
-}));
diff --git a/src/lib/stores/useLogRecentItem.ts b/src/lib/stores/useLogRecentItem.ts
deleted file mode 100644
index 20cb4394..00000000
--- a/src/lib/stores/useLogRecentItem.ts
+++ /dev/null
@@ -1,24 +0,0 @@
-import { useEffect } from "react";
-import type { UseBoundStore, StoreApi } from "zustand";
-
-type StoreWithAddItem = UseBoundStore<
- StoreApi<{
- addItem: (newItem: { id: string; item: T }) => void;
- }>
->;
-
-export function useLogRecentItem(
- item: T | null | undefined,
- useStore: StoreWithAddItem
-) {
- const addItem = useStore((state) => state.addItem);
-
- useEffect(() => {
- if (item) {
- addItem({
- id: item.id,
- item: item,
- });
- }
- }, [item, addItem]);
-}
diff --git a/src/lib/stores/useModalStore.ts b/src/lib/stores/useModalStore.ts
deleted file mode 100644
index f3c9afba..00000000
--- a/src/lib/stores/useModalStore.ts
+++ /dev/null
@@ -1,27 +0,0 @@
-import { create } from "zustand";
-
-//modal ๊ตฌ๋
-export type ModalKey = "loginModal";
-
-export type ModalState = Record;
-
-interface ModalStoreActions {
- openModal: (name: ModalKey) => void;
- closeModal: (name: ModalKey) => void;
- toggleModal: (name: ModalKey) => void;
-}
-
-const initialState: ModalState = {
- loginModal: false,
-};
-
-export const useModalStore = create((set) => ({
- ...initialState,
- openModal: (name) => set({ [name]: true }),
- closeModal: (name) => set({ [name]: false }),
- toggleModal: (name) => set((state) => ({ [name]: !state[name] })),
-}));
-
-export const MODAL_KEYS: Record = {
- LOGIN: "loginModal",
-};
diff --git a/src/lib/stores/useRecentPerfumesStore.ts b/src/lib/stores/useRecentPerfumesStore.ts
deleted file mode 100644
index b53d671d..00000000
--- a/src/lib/stores/useRecentPerfumesStore.ts
+++ /dev/null
@@ -1,21 +0,0 @@
-import { createRecentItemsStore } from "./createRecentItemsStore";
-import type { GenericRecentItem } from "./createRecentItemsStore";
-
-type RecentPerfumeData = {
- id: string;
- perfumeName: string;
- brandName: string;
- imageUrl: string;
-};
-
-export type RecentPerfumeItem = GenericRecentItem;
-
-const MAX_RECENT_PERFUMES = 10;
-
-export const useRecentPerfumesStore = createRecentItemsStore(
- {
- name: "recent-perfumes",
- maxItems: MAX_RECENT_PERFUMES,
- type: "perfume",
- }
-);
diff --git a/src/lib/stores/useRecentPostsStore.ts b/src/lib/stores/useRecentPostsStore.ts
deleted file mode 100644
index cb7828f1..00000000
--- a/src/lib/stores/useRecentPostsStore.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-import { createRecentItemsStore } from "./createRecentItemsStore";
-import type { GenericRecentItem } from "./createRecentItemsStore";
-import type { ApiPostDetailResponse } from "@/lib/hono/schemas/community.schema";
-
-export type RecentPostItem = GenericRecentItem;
-
-const MAX_RECENT_POSTS = 10;
-
-export const useRecentPostsStore =
- createRecentItemsStore({
- name: "recent-posts",
- maxItems: MAX_RECENT_POSTS,
- type: "post",
- });
diff --git a/src/lib/stores/useUserStore.ts b/src/lib/stores/useUserStore.ts
deleted file mode 100644
index 039ff282..00000000
--- a/src/lib/stores/useUserStore.ts
+++ /dev/null
@@ -1,16 +0,0 @@
-import { create } from "zustand";
-import { ApiMyProfileResponse } from "../hono/schemas/me.schema";
-
-interface UserState {
- user: ApiMyProfileResponse | null;
- isLoading: boolean;
- setUser: (user: ApiMyProfileResponse | null) => void;
- reset: () => void;
-}
-
-export const useUserStore = create((set) => ({
- user: null,
- isLoading: true,
- setUser: (user) => set({ user, isLoading: false }),
- reset: () => set({ user: null, isLoading: false }),
-}));
diff --git a/src/lib/stores/useVisibilityStore.ts b/src/lib/stores/useVisibilityStore.ts
deleted file mode 100644
index 6c2d0d72..00000000
--- a/src/lib/stores/useVisibilityStore.ts
+++ /dev/null
@@ -1,60 +0,0 @@
-import { create } from "zustand";
-
-interface VisibilityState {
- isOpen: boolean;
- type: "modal" | "dropdown" | (string & {});
-}
-
-interface VisibilityStore {
- visibilities: Record;
- open: (id: string, type?: string) => void;
- close: (id: string) => void;
- toggle: (id: string) => void;
- isOpen: (id: string) => boolean;
-}
-
-export const useVisibilityStore = create((set, get) => ({
- visibilities: {},
-
- open: (id, type = "dropdown") =>
- set((state) => {
- const newVisibilities = { ...state.visibilities };
-
- if (type === "modal") {
- for (const key in newVisibilities) {
- if (newVisibilities[key].type === "modal") {
- newVisibilities[key] = { ...newVisibilities[key], isOpen: false };
- }
- }
- }
-
- newVisibilities[id] = { isOpen: true, type };
-
- return { visibilities: newVisibilities };
- }),
-
- close: (id) =>
- set((state) => ({
- visibilities: {
- ...state.visibilities,
- [id]: {
- ...state.visibilities[id],
- isOpen: false,
- },
- },
- })),
-
- toggle: (id) => {
- const current = get().visibilities[id];
- const isOpen = current?.isOpen ?? false;
- const type = current?.type ?? "dropdown";
-
- if (isOpen) {
- get().close(id);
- } else {
- get().open(id, type);
- }
- },
-
- isOpen: (id) => get().visibilities[id]?.isOpen ?? false,
-}));
diff --git a/src/lib/utils/api/client.ts b/src/lib/utils/api/client.ts
deleted file mode 100644
index 6b70f0f8..00000000
--- a/src/lib/utils/api/client.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-import { createHttpClient } from "@/lib/utils/core-request";
-
-/**
- * ์ค์ API ํด๋ผ์ด์ธํธ
- * ๋ชจ๋ API ์์ฒญ์์ ๊ณต์ ํ๋ ๋จ์ผ ์ธ์คํด์ค
- */
-export const apiClient = createHttpClient({
- baseUrl:
- process.env.NEXT_PUBLIC_API_BASE_URL || "http://localhost:3000/api/v1",
-});
diff --git a/src/lib/utils/api/draft.api.ts b/src/lib/utils/api/draft.api.ts
deleted file mode 100644
index aafbe93c..00000000
--- a/src/lib/utils/api/draft.api.ts
+++ /dev/null
@@ -1,63 +0,0 @@
-import { apiClient } from "./client";
-import { ApiSuccessResponse } from "@/lib/hono/schemas/common.schema";
-import {
- CreateDraftBody,
- ApiDraftResponse,
- ApiDraftListResponse,
-} from "@/lib/hono/schemas/draft.schema";
-
-export const DRAFT_URL = `/drafts`;
-
-export const draftApi = {
- /**
- * ์์ ์ ์ฅ ์์ฑ/์
๋ฐ์ดํธ (Upsert)
- */
- create: (data: CreateDraftBody) => {
- return apiClient.post<
- CreateDraftBody,
- ApiSuccessResponse
- >(`${DRAFT_URL}`, data);
- },
-
- /**
- * ์์ ์ ์ฅ ๋ชฉ๋ก ์กฐํ
- */
- list: () => {
- return apiClient.get>(
- `${DRAFT_URL}`,
- undefined,
- { cache: "no-store" }
- );
- },
-
- /**
- * ํน์ ์์ ์ ์ฅ ์กฐํ
- */
- get: (id: string) => {
- return apiClient.get>(
- `${DRAFT_URL}/${id}`,
- undefined,
- { cache: "no-store" }
- );
- },
-
- /**
- * ์์ ์ ์ฅ ์ญ์
- */
- delete: (id: string) => {
- const url = `${DRAFT_URL}/${id}`;
- console.log("[draftApi.delete] DELETE ์์ฒญ:", url);
- return apiClient
- .delete>(url, {
- cache: "no-store", // ์บ์ ๋นํ์ฑํ
- })
- .then((response) => {
- console.log("[draftApi.delete] DELETE ์๋ต:", response);
- return response;
- })
- .catch((error) => {
- console.error("[draftApi.delete] DELETE ์๋ฌ:", error);
- throw error;
- });
- },
-};
diff --git a/src/lib/utils/core-request/README.md b/src/lib/utils/core-request/README.md
deleted file mode 100644
index 2ca1bfbb..00000000
--- a/src/lib/utils/core-request/README.md
+++ /dev/null
@@ -1,342 +0,0 @@
-# HTTP Client Library
-
-TypeScript ๊ธฐ๋ฐ์ ๊ฐ๋ ฅํ๊ณ ์ ์ฐํ HTTP ํด๋ผ์ด์ธํธ ๋ผ์ด๋ธ๋ฌ๋ฆฌ์
๋๋ค. ์ธํฐ์
ํฐ, ์๋ ํ ํฐ ๊ฐฑ์ , ํ์
์์ ์ฑ์ ์ ๊ณตํฉ๋๋ค.
-
-## ์ฃผ์ ๊ธฐ๋ฅ
-
-- โจ **ํ์
์์ ์ฑ**: ์์ ํ TypeScript ์ง์
-- ๐ **์๋ ํ ํฐ ๊ฐฑ์ **: 401 ์๋ต ์ ์๋์ผ๋ก ํ ํฐ ๊ฐฑ์ ๋ฐ ์ฌ์๋
-- ๐ฏ **์ธํฐ์
ํฐ**: ์์ฒญ/์๋ต ์ธํฐ์
ํฐ๋ฅผ ํตํ ์ปค์คํฐ๋ง์ด์ง
-- ๐ง **์๋ต ๋ณํ**: ์๋ต ๋ฐ์ดํฐ ๋ณํ ๊ธฐ๋ฅ
-- ๐ **Next.js ์๋ฒ ์ง์**: Next.js ์๋ฒ ์ปดํฌ๋ํธ๋ฅผ ์ํ ์ ์ฉ ํด๋ผ์ด์ธํธ
-- ๐ **FormData ์ง์**: JSON๊ณผ FormData ๋ชจ๋ ์ง์
-
-## ๊ธฐ๋ณธ ์ฌ์ฉ๋ฒ
-
-### ํด๋ผ์ด์ธํธ ์์ฑ
-
-```typescript
-import { createHttpClient } from "your-http-client-library";
-
-const apiClient = createHttpClient({
- baseUrl: "https://api.example.com",
- headers: {
- "Content-Type": "application/json",
- },
- debug: true, // ๋๋ฒ๊ทธ ๋ก๊ทธ ํ์ฑํ (์ ํ)
-});
-```
-
-### GET ์์ฒญ
-
-```typescript
-// ๊ธฐ๋ณธ GET ์์ฒญ
-const users = await apiClient.get("/users");
-
-// ์ฟผ๋ฆฌ ํ๋ผ๋ฏธํฐ ์ฌ์ฉ
-const filteredUsers = await apiClient.get("/users", {
- page: 1,
- limit: 10,
- status: "active",
-});
-
-// ํ์
์์ ์ฑ๊ณผ ํจ๊ป ์ฌ์ฉ
-interface User {
- id: number;
- name: string;
- email: string;
-}
-
-const users = await apiClient.get("/users");
-```
-
-### POST ์์ฒญ
-
-```typescript
-// JSON ๋ฐ์ดํฐ ์ ์ก
-interface CreateUserRequest {
- name: string;
- email: string;
-}
-
-interface UserResponse {
- id: number;
- name: string;
- email: string;
- createdAt: string;
-}
-
-const newUser = await apiClient.post(
- "/users",
- {
- name: "John Doe",
- email: "john@example.com",
- }
-);
-
-// FormData ์ ์ก
-const formData = new FormData();
-formData.append("file", file);
-formData.append("title", "My File");
-
-const uploadResult = await apiClient.post("/upload", formData);
-```
-
-### PUT / PATCH ์์ฒญ
-
-```typescript
-// PUT ์์ฒญ (์ ์ฒด ์
๋ฐ์ดํธ)
-const updatedUser = await apiClient.put("/users/123", {
- name: "Jane Doe",
- email: "jane@example.com",
-});
-
-// PATCH ์์ฒญ (๋ถ๋ถ ์
๋ฐ์ดํธ)
-const partialUpdate = await apiClient.patch("/users/123", {
- name: "Jane Smith",
-});
-```
-
-### DELETE ์์ฒญ
-
-```typescript
-// ์ญ์ ์์ฒญ
-await apiClient.delete("/users/123");
-```
-
-## ๊ณ ๊ธ ๊ธฐ๋ฅ
-
-### ์๋ต ๋ฐ์ดํฐ ๋ณํ
-
-```typescript
-interface ApiResponse {
- data: User[];
- meta: { total: number };
-}
-
-const users = await apiClient.get(
- "/users",
- {},
- {
- transformResponse: (response) => response.data,
- }
-);
-// users๋ User[] ํ์
-```
-
-### ์์ฒญ ์ธํฐ์
ํฐ
-
-```typescript
-// ๋ชจ๋ ์์ฒญ์ Authorization ํค๋ ์ถ๊ฐ
-apiClient.useRequestInterceptor((config) => {
- const token = localStorage.getItem("token");
- const headers = new Headers(config.headers);
-
- if (token) {
- headers.set("Authorization", `Bearer ${token}`);
- }
-
- return {
- ...config,
- headers,
- };
-});
-```
-
-### ์๋ต ์ธํฐ์
ํฐ
-
-```typescript
-// ๋ชจ๋ ์๋ต ๋ก๊น
-apiClient.useResponseInterceptor((response) => {
- console.log("Response status:", response.status);
- return response;
-});
-
-// ์๋ฌ ์๋ต ์ฒ๋ฆฌ
-apiClient.useResponseInterceptor(async (response) => {
- if (response.status === 403) {
- console.error("Access forbidden");
- // ์ถ๊ฐ ์ฒ๋ฆฌ ๋ก์ง
- }
- return response;
-});
-```
-
-### ์๋ ํ ํฐ ๊ฐฑ์
-
-```typescript
-const apiClient = createHttpClient({
- baseUrl: "https://api.example.com",
- refreshToken: async () => {
- // ํ ํฐ ๊ฐฑ์ ๋ก์ง
- const response = await fetch("/auth/refresh", {
- method: "POST",
- credentials: "include",
- });
-
- if (!response.ok) {
- throw new Error("Token refresh failed");
- }
-
- const data = await response.json();
- localStorage.setItem("token", data.accessToken);
- },
-});
-
-// 401 ์๋ต ์ ์๋์ผ๋ก ํ ํฐ์ ๊ฐฑ์ ํ๊ณ ์์ฒญ์ ์ฌ์๋ํฉ๋๋ค
-const data = await apiClient.get("/protected-resource");
-```
-
-### Next.js ์๋ฒ ์ปดํฌ๋ํธ ์ฌ์ฉ
-
-```typescript
-import { createApiServerClient } from "your-http-client-library/serverClient";
-
-export default async function ServerComponent() {
- const apiClient = await createApiServerClient();
-
- const data = await apiClient.get("/server-data");
-
- return {/* ๋ ๋๋ง ๋ก์ง */}
;
-}
-```
-
-## ์๋ฌ ์ฒ๋ฆฌ
-
-```typescript
-import { APIError } from "your-http-client-library";
-
-try {
- const data = await apiClient.get("/users");
-} catch (error) {
- if (error instanceof APIError) {
- console.error("API Error:", {
- status: error.status,
- message: error.message,
- data: error.errorData,
- });
-
- // ์ํ ์ฝ๋๋ณ ์ฒ๋ฆฌ
- if (error.status === 404) {
- console.error("Resource not found");
- } else if (error.status === 500) {
- console.error("Server error");
- }
- } else {
- console.error("Unknown error:", error);
- }
-}
-```
-
-## ์ค์ ์ต์
-
-### LibraryConfig
-
-```typescript
-interface LibraryConfig {
- baseUrl: string; // API ๊ธฐ๋ณธ URL (ํ์)
- headers?: HeadersInit; // ๊ธฐ๋ณธ ํค๋
- tokenProvider?: () => string | null; // ํ ํฐ ์ ๊ณต ํจ์
- refreshToken?: () => Promise; // ํ ํฐ ๊ฐฑ์ ํจ์
- debug?: boolean; // ๋๋ฒ๊ทธ ๋ชจ๋
-}
-```
-
-### ์์ฒญ ์ต์
-
-```typescript
-interface GetApiOptions {
- cache?: RequestCache; // ์บ์ ์ค์
- headers?: HeadersInit; // ์ถ๊ฐ ํค๋
- skipAuth?: boolean; // ์ธ์ฆ ์คํต
- transformResponse?: (response: RawData) => TransformedData;
-}
-
-interface MutateApiOptions {
- cache?: RequestCache;
- headers?: HeadersInit;
- transformResponse?: (response: RawData) => TransformedData;
-}
-```
-
-## ์์
-
-### ์์ ํ ์ธ์ฆ ํ๋ก์ฐ ์์
-
-```typescript
-const apiClient = createHttpClient({
- baseUrl: "https://api.example.com",
- debug: true,
- refreshToken: async () => {
- const response = await fetch("https://api.example.com/auth/refresh", {
- method: "POST",
- credentials: "include",
- });
-
- if (!response.ok) {
- window.location.href = "/login";
- throw new Error("Refresh failed");
- }
-
- const data = await response.json();
- localStorage.setItem("accessToken", data.accessToken);
- },
-});
-
-// Authorization ํค๋ ์๋ ์ถ๊ฐ
-apiClient.useRequestInterceptor((config) => {
- const token = localStorage.getItem("accessToken");
- const headers = new Headers(config.headers);
-
- if (token && !config.skipAuth) {
- headers.set("Authorization", `Bearer ${token}`);
- }
-
- return { ...config, headers };
-});
-
-// ์ฌ์ฉ
-const userData = await apiClient.get("/user/me");
-```
-
-### React์์ ์ฌ์ฉํ๊ธฐ
-
-```typescript
-// hooks/useApi.ts
-import { createHttpClient } from "your-http-client-library";
-import { useMemo } from "react";
-
-export function useApiClient() {
- return useMemo(() => {
- const client = createHttpClient({
- baseUrl: process.env.NEXT_PUBLIC_API_URL!,
- refreshToken: async () => {
- // ํ ํฐ ๊ฐฑ์ ๋ก์ง
- },
- });
-
- client.useRequestInterceptor((config) => {
- // ์ธํฐ์
ํฐ ๋ก์ง
- return config;
- });
-
- return client;
- }, []);
-}
-
-// ์ปดํฌ๋ํธ์์ ์ฌ์ฉ
-function UserList() {
- const apiClient = useApiClient();
- const [users, setUsers] = useState([]);
-
- useEffect(() => {
- apiClient.get("/users").then(setUsers);
- }, [apiClient]);
-
- return {/* ๋ ๋๋ง */}
;
-}
-```
-
-## ๋ผ์ด์ผ์ค
-
-MIT
diff --git a/src/lib/utils/core-request/config.ts b/src/lib/utils/core-request/config.ts
deleted file mode 100644
index 3dfbade4..00000000
--- a/src/lib/utils/core-request/config.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-export interface LibraryConfig {
- baseUrl: string;
- headers?: HeadersInit;
- tokenProvider?: () => string | null;
- refreshToken?: () => Promise;
- debug?: boolean;
-}
diff --git a/src/lib/utils/core-request/core.ts b/src/lib/utils/core-request/core.ts
deleted file mode 100644
index a91c0a27..00000000
--- a/src/lib/utils/core-request/core.ts
+++ /dev/null
@@ -1,116 +0,0 @@
-import type { LibraryConfig } from "./config";
-import { manageTokenRefresh } from "./token";
-
-// --- ํ์
์ ์ ---
-export interface CustomRequestInit extends RequestInit {
- skipAuth?: boolean;
-}
-
-type RequestInterceptor = (
- config: CustomRequestInit
-) => Promise | CustomRequestInit;
-type ResponseInterceptor = (response: Response) => Promise | Response;
-
-export interface RequestOptions
- extends CustomRequestInit {
- url: string;
- isRetry?: boolean;
- transformResponse?: (response: ResponseData) => TransformedData;
-}
-
-// --- ์ปค์คํ
์๋ฌ ํด๋์ค ---
-export class APIError extends Error {
- constructor(
- public status: number,
- public message: string,
- public errorData?: unknown
- ) {
- super(message);
- this.name = "APIError";
- }
-}
-
-// --- HttpClient ํฉํ ๋ฆฌ ํจ์ ---
-export function createCoreClient(config: LibraryConfig) {
- const requestInterceptors: RequestInterceptor[] = [];
- const responseInterceptors: ResponseInterceptor[] = [];
-
- // ์์ฒญ ์ธํฐ์
ํฐ ํจ์
- const useRequestInterceptor = (interceptor: RequestInterceptor) => {
- requestInterceptors.push(interceptor);
- };
-
- // ์๋ต ์ธํฐ์
ํฐ ํจ์
- const useResponseInterceptor = (interceptor: ResponseInterceptor) => {
- responseInterceptors.push(interceptor);
- };
-
- // HTTP ์์ฒญ์ ์ํํ๋ ํจ์
- const request = async (
- originalOptions: RequestOptions
- ): Promise => {
- const { url, transformResponse, ...options } = originalOptions;
-
- let reqConfig: CustomRequestInit = { ...options };
- for (const interceptor of requestInterceptors) {
- reqConfig = await interceptor(reqConfig);
- }
-
- const headers = new Headers(reqConfig.headers);
-
- if (config.debug) {
- console.log(`[Request] Calling fetch for URL: ${url}`);
- try {
- console.log(
- `[Request] Fetch options:`,
- JSON.stringify(reqConfig, null, 2)
- );
- } catch {
- console.log(`[Request] Fetch options:`, reqConfig);
- }
- }
-
- let response = await fetch(url, {
- ...reqConfig,
- headers,
- cache: reqConfig.cache || "force-cache",
- next: { revalidate: 300 },
- });
-
- if (response.status === 401 && !options.isRetry && config.refreshToken) {
- await manageTokenRefresh(config.refreshToken);
- return await request({ ...originalOptions, isRetry: true });
- }
-
- for (const interceptor of responseInterceptors) {
- response = await interceptor(response);
- }
-
- // ์๋ต ์ํ ์ฒดํฌ
- if (!response.ok) {
- let errorData;
- try {
- errorData = await response.json();
- } catch {
- errorData = { message: response.statusText || "Unknown error" };
- }
- throw new APIError(response.status, errorData.message, errorData);
- }
-
- if (response.status === 204) return null;
-
- const data = (await response.json()) as RawData;
-
- if (transformResponse) {
- return transformResponse(data);
- }
-
- return data as unknown as TransformedData;
- };
-
- return {
- useRequestInterceptor,
- useResponseInterceptor,
- request,
- };
-}
diff --git a/src/lib/utils/core-request/httpClient.ts b/src/lib/utils/core-request/httpClient.ts
deleted file mode 100644
index dab0015e..00000000
--- a/src/lib/utils/core-request/httpClient.ts
+++ /dev/null
@@ -1,186 +0,0 @@
-import { createCoreClient } from "./core";
-import type { LibraryConfig } from "./config";
-
-export interface BaseApiOptions {
- cache?: RequestCache;
- headers?: HeadersInit;
-}
-
-export interface GetApiOptions
- extends BaseApiOptions {
- skipAuth?: boolean;
- transformResponse?: (response: RawData) => TransformedData;
-}
-
-export interface MutateApiOptions
- extends BaseApiOptions {
- transformResponse?: (response: RawData) => TransformedData;
-}
-
-function objectToQueryString(params: Record): string {
- const entries = Object.entries(params);
- const filteredEntries = entries.filter(
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
- ([_key, value]) => value !== undefined && value !== null
- );
- const cleanedParams = Object.fromEntries(filteredEntries);
-
- return new URLSearchParams(
- cleanedParams as Record
- ).toString();
-}
-
-export function createHttpClient(config: LibraryConfig) {
- const core = createCoreClient(config);
-
- const mergeHeaders = (
- baseHeaders: HeadersInit,
- userHeaders?: HeadersInit
- ): Headers => {
- const result = new Headers(baseHeaders);
- if (userHeaders) {
- new Headers(userHeaders).forEach((value, key) => {
- result.set(key, value);
- });
- }
- return result;
- };
-
- const mutate = <
- RequestData extends object,
- RawData,
- TransformedData = RawData
- >(
- method: "PUT" | "PATCH",
- path: string,
- data: RequestData,
- options?: MutateApiOptions
- ) => {
- const finalUrl = `${config.baseUrl}${path}`;
-
- const headers = mergeHeaders(
- { "Content-Type": "application/json" },
- options?.headers
- );
-
- return core.request({
- ...options,
- url: finalUrl,
- method,
- body: JSON.stringify(data),
- headers,
- });
- };
-
- const get = (
- path: string,
- params?: Record,
- options?: GetApiOptions
- ) => {
- let finalUrl = `${config.baseUrl}${path}`;
- if (params && Object.keys(params).length > 0) {
- finalUrl += `?${objectToQueryString(params)}`;
- }
- return core.request({
- ...options,
- url: finalUrl,
- method: "GET",
- cache: options?.cache || "force-cache",
- });
- };
-
- // Overloading ์ ์
- function post(
- path: string,
- data: FormData,
- options?: MutateApiOptions
- ): Promise;
- function post(
- path: string,
- data: RequestData,
- options?: MutateApiOptions
- ): Promise;
-
- // Overloading ๊ตฌํ ์ฝ๋
- function post(
- path: string,
- data: RequestData | FormData,
- options?: MutateApiOptions
- ): Promise {
- const finalUrl = `${config.baseUrl}${path}`;
-
- if (data instanceof FormData) {
- return core.request({
- ...options,
- url: finalUrl,
- method: "POST",
- body: data,
- });
- } else {
- const headers = mergeHeaders(
- { "Content-Type": "application/json" },
- options?.headers
- );
- return core.request({
- ...options,
- url: finalUrl,
- method: "POST",
- body: JSON.stringify(data),
- headers,
- });
- }
- }
-
- const put = (
- path: string,
- data: RequestData,
- options?: MutateApiOptions
- ) => {
- return mutate(
- "PUT",
- path,
- data,
- options
- );
- };
-
- const patch = <
- RequestData extends object,
- RawData,
- TransformedData = RawData
- >(
- path: string,
- data: RequestData,
- options?: MutateApiOptions
- ) => {
- return mutate(
- "PATCH",
- path,
- data,
- options
- );
- };
-
- const remove = (
- path: string,
- options?: MutateApiOptions
- ) => {
- const finalUrl = `${config.baseUrl}${path}`;
- return core.request({
- ...options,
- url: finalUrl,
- method: "DELETE",
- });
- };
-
- return Object.freeze({
- request: core.request,
- useRequestInterceptor: core.useRequestInterceptor,
- useResponseInterceptor: core.useResponseInterceptor,
- get,
- post,
- put,
- patch,
- delete: remove,
- });
-}
diff --git a/src/lib/utils/core-request/index.ts b/src/lib/utils/core-request/index.ts
deleted file mode 100644
index 94ad682a..00000000
--- a/src/lib/utils/core-request/index.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-export { createHttpClient } from "./httpClient";
-export type { LibraryConfig } from "./config";
-export { APIError } from "./core";
-
-export type {
- GetApiOptions,
- MutateApiOptions,
- BaseApiOptions,
-} from "./httpClient";
diff --git a/src/lib/utils/core-request/serverClient.ts b/src/lib/utils/core-request/serverClient.ts
deleted file mode 100644
index 8f31af26..00000000
--- a/src/lib/utils/core-request/serverClient.ts
+++ /dev/null
@@ -1,29 +0,0 @@
-import { cookies } from "next/headers";
-import { createHttpClient } from "./httpClient";
-
-export const createApiServerClient = async () => {
- const cookieStore = await cookies();
- const sessionToken =
- cookieStore.get("authjs.session-token")?.value ||
- cookieStore.get("__Secure-authjs.session-token")?.value;
-
- const apiClient = createHttpClient({
- baseUrl:
- process.env.NEXT_PUBLIC_API_BASE_URL || "http://localhost:3000/api/v1",
- headers: sessionToken ? { cookie: sessionToken } : undefined,
- });
- apiClient.useRequestInterceptor((config) => {
- const headers = new Headers(config.headers);
-
- const cookieString = cookieStore
- .getAll()
- .map((cookie) => `${cookie.name}=${cookie.value}`)
- .join("; ");
- headers.set("cookie", cookieString);
- return {
- ...config,
- headers,
- };
- });
- return apiClient;
-};
diff --git a/src/lib/utils/core-request/token.ts b/src/lib/utils/core-request/token.ts
deleted file mode 100644
index 15d90d21..00000000
--- a/src/lib/utils/core-request/token.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-let isRefreshing = false;
-let refreshPromise: Promise | null = null;
-
-type RefreshFn = () => Promise;
-
-/**
- * ํ ํฐ ๊ฐฑ์ ์ ๊ด๋ฆฌํ๋ ํจ์
- * @param refreshToken ํ ํฐ ๊ฐฑ์ ํจ์
- * @returns ํ ํฐ ๊ฐฑ์ ์ด ์๋ฃ๋ ๋๊น์ง์ Promise
- */
-export function manageTokenRefresh(refreshToken: RefreshFn): Promise {
- if (!isRefreshing) {
- isRefreshing = true;
- refreshPromise = refreshToken().finally(() => {
- isRefreshing = false;
- });
- }
-
- return refreshPromise!;
-}
diff --git a/src/middleware.ts b/src/middleware.ts
index 712a916a..f87f5d46 100644
--- a/src/middleware.ts
+++ b/src/middleware.ts
@@ -1,85 +1,93 @@
+import NextAuth from "next-auth";
import { NextResponse } from "next/server";
-import type { NextRequest } from "next/server";
+import { authConfig } from "@/authConfig";
+
+const { auth } = NextAuth(authConfig);
-// ๋น๋ก๊ทธ์ธ ์ํ์์๋ ์ ๊ทผ ๊ฐ๋ฅํ ํ์ด์ง ๊ฒฝ๋ก
const PUBLIC_ROUTES = ["/", "/perfumes", "/community"];
-// ๋์ ๊ฒฝ๋ก ํจํด (๋น๋ก๊ทธ์ธ ํ์ฉ)
const PUBLIC_ROUTE_PATTERNS = [
- /^\/perfumes\/[^/]+$/, // /perfumes/:id
- /^\/brand\/[^/]+$/, // /brand/:name
- /^\/community\/post\/[^/]+$/, // /community/post/:id (๊ฒ์๊ธ ์ฝ๊ธฐ)
- /^\/user\/[^/]+$/, // /user/:id (ํ๋กํ ๋ณด๊ธฐ)
- /^\/user\/[^/]+\/collection$/, // /user/:id/collection
- /^\/user\/[^/]+\/bookmarks$/, // /user/:id/bookmarks
+ /^\/perfumes\/[^/]+$/,
+ /^\/brand\/[^/]+$/,
+ /^\/community\/post\/[^/]+$/,
+ /^\/user\/[^/]+$/,
+ /^\/user\/[^/]+\/collection$/,
+ /^\/user\/[^/]+\/bookmarks$/,
];
-/**
- * ์ฃผ์ด์ง ๊ฒฝ๋ก๊ฐ ๋น๋ก๊ทธ์ธ ์ํ์์ ์ ๊ทผ ๊ฐ๋ฅํ์ง ํ์ธ
- */
-function isPublicRoute(pathname: string): boolean {
- if (PUBLIC_ROUTES.includes(pathname)) {
- return true;
- }
+const SESSION_COOKIE_NAMES = [
+ "authjs.session-token",
+ "__Secure-authjs.session-token",
+];
+function isPublicRoute(pathname: string): boolean {
+ if (PUBLIC_ROUTES.includes(pathname)) return true;
return PUBLIC_ROUTE_PATTERNS.some((pattern) => pattern.test(pathname));
}
-export async function middleware(request: NextRequest) {
- const { pathname } = request.nextUrl;
-
- // API ๋ผ์ฐํธ ์ ์ธ
- if (pathname.startsWith("/api/")) {
- return NextResponse.next();
- }
-
- // Next.js ๋ด๋ถ ๊ฒฝ๋ก ์ ์ธ (__nextjs ๊ด๋ จ ๋ชจ๋ ์ ์ธ)
- if (
- pathname.startsWith("/_next") ||
- pathname.startsWith("/__nextjs") ||
- pathname.startsWith("/static")
- ) {
- return NextResponse.next();
- }
-
- // ์ ์ ํ์ผ ์ ์ธ
- if (pathname.includes(".")) {
- return NextResponse.next();
- }
+export default auth((req) => {
+ const { pathname } = req.nextUrl;
+ const isAuthenticated = !!req.auth;
+ const isPublic = isPublicRoute(pathname);
- const sessionToken =
- request.cookies.get("authjs.session-token")?.value ||
- request.cookies.get("__Secure-authjs.session-token")?.value;
+ // ์ธ์
์ฟ ํค๊ฐ ์กด์ฌํ์ง๋ง ํ์ฑ์ ์คํจํ ๊ฒฝ์ฐ(์์๋ ํ ํฐ, ์๋ชป๋ ์ํฌ๋ฆฟ ๋ฑ)
+ // req.auth๊ฐ null์ด์ง๋ง ์ฟ ํค๊ฐ ๋จ์์๋ ์ํ โ ์ฟ ํค๋ฅผ ์ง์์ ์ด๊ธฐํ
+ const hasSessionCookie = SESSION_COOKIE_NAMES.some((name) =>
+ req.cookies.has(name),
+ );
+
+ if (!isAuthenticated && hasSessionCookie) {
+ if (!isPublic) {
+ const url = req.nextUrl.clone();
+ const originalUrl = req.nextUrl.search
+ ? `${pathname}${req.nextUrl.search}`
+ : pathname;
+
+ if (pathname.startsWith("/community")) {
+ url.pathname = "/community";
+ const referer = req.headers.get("referer");
+ if (referer) {
+ const refererParams = new URL(referer).search;
+ if (refererParams.startsWith("?")) {
+ url.search = refererParams.substring(1);
+ }
+ }
+ } else {
+ url.pathname = "/";
+ url.search = "";
+ }
- const isPublic = isPublicRoute(pathname);
+ url.searchParams.set("callbackUrl", originalUrl);
+ const redirect = NextResponse.redirect(url);
+ for (const name of SESSION_COOKIE_NAMES) {
+ redirect.cookies.delete(name);
+ }
+ return redirect;
+ }
- // ๊ฐ๋ฐ ๋ชจ๋์์๋ง ๋ก๊น
(production์์๋ ๋ก๊น
์ ๊ฑฐ)
- if (process.env.NODE_ENV === "development") {
- console.log("๐ [Middleware]", pathname, {
- session: !!sessionToken,
- public: isPublic,
- });
+ // ํผ๋ธ๋ฆญ ๋ผ์ฐํธ์ธ ๊ฒฝ์ฐ ๋ฆฌ๋๋ ํธ ์์ด ์ฟ ํค๋ง ์ด๊ธฐํ
+ const next = NextResponse.next();
+ for (const name of SESSION_COOKIE_NAMES) {
+ next.cookies.delete(name);
+ }
+ return next;
}
- // ๋ก๊ทธ์ธ ํ์ํ ํ์ด์ง์ ๋น๋ก๊ทธ์ธ ์ํ๋ก ์ ๊ทผ
- if (!sessionToken && !isPublic) {
- const url = request.nextUrl.clone();
+ if (!isAuthenticated && !isPublic) {
+ const url = req.nextUrl.clone();
- const originalUrl = request.nextUrl.search
- ? `${pathname}${request.nextUrl.search}`
+ const originalUrl = req.nextUrl.search
+ ? `${pathname}${req.nextUrl.search}`
: pathname;
- const referer = request.headers.get("referer");
- let refererParams = "";
- if (referer) {
- const refererUrl = new URL(referer);
- refererParams = refererUrl.search;
- }
-
if (pathname.startsWith("/community")) {
url.pathname = "/community";
- if (refererParams && refererParams.startsWith("?")) {
- url.search = refererParams.substring(1);
+ const referer = req.headers.get("referer");
+ if (referer) {
+ const refererParams = new URL(referer).search;
+ if (refererParams.startsWith("?")) {
+ url.search = refererParams.substring(1);
+ }
}
} else {
url.pathname = "/";
@@ -87,26 +95,12 @@ export async function middleware(request: NextRequest) {
}
url.searchParams.set("callbackUrl", originalUrl);
-
- if (process.env.NODE_ENV === "development") {
- console.log("๐ซ [Middleware] REDIRECT:", originalUrl, "โ", url.pathname);
- }
-
- return NextResponse.redirect(url);
+ return Response.redirect(url);
}
-
- return NextResponse.next();
-}
+});
export const config = {
matcher: [
- /*
- * Match all request paths except:
- * - api (API routes)
- * - _next (Next.js internals)
- * - __nextjs (Next.js dev tools)
- * - static files (images, fonts, etc.)
- */
"/((?!api|_next|__nextjs|favicon.ico|sitemap.xml|robots.txt|.*\\.(?:png|jpg|jpeg|gif|webp|svg|ico|woff|woff2|ttf|eot)).*)",
],
};
diff --git a/src/lib/database/action/login.ts b/src/server/database/action/login.ts
similarity index 60%
rename from src/lib/database/action/login.ts
rename to src/server/database/action/login.ts
index 7567d3b2..6b7e8492 100644
--- a/src/lib/database/action/login.ts
+++ b/src/server/database/action/login.ts
@@ -1,22 +1,20 @@
"use server";
-import { signIn, signOut } from "@/auth";
-import { revalidatePath } from "next/cache";
+import { signIn } from "@/auth";
export const naverLogin = async (callbackUrl?: string) => {
await signIn("naver", { redirect: true, redirectTo: callbackUrl || "/" });
};
export const googleLogin = async (callbackUrl?: string) => {
- await signIn("google", { redirect: true, redirectTo: callbackUrl || "/" });
+ await signIn(
+ "google",
+ { redirect: true, redirectTo: callbackUrl || "/" },
+ { prompt: "select_account" },
+ );
};
// TODO: kakao email ๋ฐ์์ค๋ ๊ถํ ์์. ๋ณด๋ฅ
export const kakaoLogin = async (callbackUrl?: string) => {
await signIn("kakao", { redirect: true, redirectTo: callbackUrl || "/" });
};
-
-export const logout = async () => {
- await signOut({ redirect: false });
- revalidatePath("/", "layout");
-};
diff --git a/src/lib/database/getSession.ts b/src/server/database/getSession.ts
similarity index 100%
rename from src/lib/database/getSession.ts
rename to src/server/database/getSession.ts
diff --git a/src/lib/hono/app.ts b/src/server/hono/app.ts
similarity index 94%
rename from src/lib/hono/app.ts
rename to src/server/hono/app.ts
index 25535263..5b7afb2b 100644
--- a/src/lib/hono/app.ts
+++ b/src/server/hono/app.ts
@@ -13,13 +13,12 @@ import fileApi from "./routes/v1/file.handler";
import filterApi from "./routes/v1/filter.handler";
import pointApi from "./routes/v1/point.handler";
import draftsApi from "./routes/v1/draft.handler";
+import authApi from "./routes/v1/auth.handler";
import { handleApiError } from "./utils/api.utils";
-export type AuthenticatedUser = User;
-
export type AppContext = {
Variables: {
- user?: AuthenticatedUser;
+ user?: User;
session?: Session;
};
};
@@ -56,6 +55,7 @@ v1.route("/users", usersApi);
v1.route("/file", fileApi);
v1.route("/points", pointApi);
v1.route("/drafts", draftsApi);
+v1.route("/auth", authApi);
app.route("/v1", v1);
diff --git a/src/lib/hono/middleware/auth.middleware.ts b/src/server/hono/middleware/auth.middleware.ts
similarity index 61%
rename from src/lib/hono/middleware/auth.middleware.ts
rename to src/server/hono/middleware/auth.middleware.ts
index e8c68084..738e0446 100644
--- a/src/lib/hono/middleware/auth.middleware.ts
+++ b/src/server/hono/middleware/auth.middleware.ts
@@ -4,8 +4,17 @@ import { apiUnauthorized } from "../utils/api.utils";
import type { AppContext } from "../app";
export const authMiddleware = createMiddleware(async (c, next) => {
- const session = await getSession();
+ console.log(`[HONO][auth] ๋ฏธ๋ค์จ์ด ์ง์
โ path=${c.req.path}`);
+ let session;
+ try {
+ session = await getSession();
+ console.log(`[HONO][auth] getSession ์๋ฃ โ userId=${session?.user?.id ?? "์์"} email=${session?.user?.email ?? "์์"}`);
+ } catch (err) {
+ console.error(`[HONO][auth] getSession ์ค๋ฅ:`, err);
+ return apiUnauthorized(c);
+ }
if (!session || !session.user) {
+ console.warn(`[HONO][auth] ์ธ์
์์ โ 401 ๋ฐํ`);
return apiUnauthorized(c);
}
c.set("user", session.user);
diff --git a/src/server/hono/repositories/base.repository.ts b/src/server/hono/repositories/base.repository.ts
new file mode 100644
index 00000000..1d977e14
--- /dev/null
+++ b/src/server/hono/repositories/base.repository.ts
@@ -0,0 +1,37 @@
+import { prisma } from "@/server/prisma";
+import {
+ serviceNotFound,
+ serviceSuccess,
+ ServiceResult,
+} from "@/server/result";
+
+type FindUniqueMethod = {
+ findUnique: (args: {
+ where: { id: string };
+ select: { id: boolean };
+ }) => Promise<{ id: string } | null>;
+};
+
+/**
+ * ํน์ ๋ฆฌ์์ค๊ฐ ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ์กด์ฌํ๋์ง ํ์ธํฉ๋๋ค.
+ * @param model - Prisma ๋ชจ๋ธ ์ด๋ฆ (์: "perfume", "user")
+ * @param id - ํ์ธํ ๋ฆฌ์์ค์ ID
+ * @param resourceName - ์ค๋ฅ ๋ฉ์์ง์ ์ฌ์ฉํ ๋ฆฌ์์ค์ ํ๊ธ ์ด๋ฆ (์: "ํฅ์")
+ */
+export async function checkResourceExists(
+ model: keyof typeof prisma,
+ id: string,
+ resourceName: string
+): Promise> {
+ const resource = await (
+ prisma[model] as unknown as FindUniqueMethod
+ ).findUnique({
+ where: { id },
+ select: { id: true },
+ });
+
+ if (!resource) {
+ return serviceNotFound(`${resourceName}์(๋ฅผ) ์ฐพ์ ์ ์์ต๋๋ค.`);
+ }
+ return serviceSuccess(true);
+}
diff --git a/src/server/hono/repositories/brand.repository.ts b/src/server/hono/repositories/brand.repository.ts
new file mode 100644
index 00000000..a9021457
--- /dev/null
+++ b/src/server/hono/repositories/brand.repository.ts
@@ -0,0 +1,64 @@
+import { Prisma } from "@prisma/client";
+
+export const brandSelect = {
+ nameEn: true,
+ nameKo: true,
+ brandUrl: true,
+} satisfies Prisma.BrandSelect;
+
+export const brandSimpleSelect = {
+ nameEn: true,
+ nameKo: true,
+} satisfies Prisma.BrandSelect;
+
+export const brandDetailSelect = {
+ id: true,
+ nameEn: true,
+ nameKo: true,
+ description: true,
+ imageUrl: true,
+ brandUrl: true,
+ mapLocation: true,
+} satisfies Prisma.BrandSelect;
+
+export const brandWithPerfumesInclude = {
+ perfumes: {
+ include: {
+ brand: { select: brandSelect },
+ perfumeImage: { select: { imageUrl: true } },
+ },
+ orderBy: { nameKo: "asc" as const },
+ },
+} satisfies Prisma.BrandInclude;
+
+export type SimpleBrand = Prisma.BrandGetPayload<{
+ select: typeof brandSimpleSelect;
+}>;
+
+export type DetailBrand = Prisma.BrandGetPayload<{
+ select: typeof brandDetailSelect;
+}>;
+
+export type BrandWithPerfumes = Prisma.BrandGetPayload<{
+ include: typeof brandWithPerfumesInclude;
+}>;
+
+export function parseMapLocation(
+ value: Prisma.JsonValue
+): { latitude: number; longitude: number } | null {
+ if (value === null) return null;
+
+ if (typeof value === "object" && value !== null && !Array.isArray(value)) {
+ const obj = value as Record;
+
+ if (typeof obj.latitude === "number" && typeof obj.longitude === "number") {
+ return {
+ latitude: obj.latitude,
+ longitude: obj.longitude,
+ };
+ }
+ }
+
+ console.warn("Invalid mapLocation format:", value);
+ return null;
+}
diff --git a/src/server/hono/repositories/comment.repository.ts b/src/server/hono/repositories/comment.repository.ts
new file mode 100644
index 00000000..3cc68a99
--- /dev/null
+++ b/src/server/hono/repositories/comment.repository.ts
@@ -0,0 +1,22 @@
+import { Prisma } from "@prisma/client";
+import { authorSelect } from "./user.repository";
+
+export const commentIncludeArgs = {
+ author: {
+ select: authorSelect,
+ },
+ replies: {
+ include: {
+ author: {
+ select: authorSelect,
+ },
+ },
+ orderBy: {
+ createdAt: "asc" as const,
+ },
+ },
+} satisfies Prisma.CommentInclude;
+
+export type CommentWithReplies = Prisma.CommentGetPayload<{
+ include: typeof commentIncludeArgs;
+}>;
diff --git a/src/server/hono/repositories/community.repository.ts b/src/server/hono/repositories/community.repository.ts
new file mode 100644
index 00000000..0bb0e11f
--- /dev/null
+++ b/src/server/hono/repositories/community.repository.ts
@@ -0,0 +1,33 @@
+import { Prisma } from "@prisma/client";
+import { authorSelect } from "./user.repository";
+import { perfumeBaseInclude } from "./perfume.repository";
+
+export const postIncludeArgs = {
+ author: {
+ select: authorSelect,
+ },
+} satisfies Prisma.PostInclude;
+
+export const postDetailIncludeArgs = {
+ ...postIncludeArgs,
+ perfumeMappings: {
+ select: {
+ perfume: {
+ select: {
+ id: true,
+ nameEn: true,
+ nameKo: true,
+ ...perfumeBaseInclude,
+ },
+ },
+ },
+ },
+} satisfies Prisma.PostInclude;
+
+export type BasePost = Prisma.PostGetPayload<{
+ include: typeof postIncludeArgs;
+}>;
+
+export type FullPost = Prisma.PostGetPayload<{
+ include: typeof postDetailIncludeArgs;
+}>;
diff --git a/src/server/hono/repositories/perfume.repository.ts b/src/server/hono/repositories/perfume.repository.ts
new file mode 100644
index 00000000..cf2bfd2f
--- /dev/null
+++ b/src/server/hono/repositories/perfume.repository.ts
@@ -0,0 +1,34 @@
+import { Prisma } from "@prisma/client";
+import { brandSelect } from "./brand.repository";
+import { authorSelect } from "./user.repository";
+
+export const perfumeBaseInclude = {
+ brand: { select: brandSelect },
+ perfumeImage: { select: { imageUrl: true } },
+} satisfies Prisma.PerfumeInclude;
+
+export const perfumeDetailInclude = {
+ ...perfumeBaseInclude,
+ accordMappings: { select: { accord: true } },
+ noteMappings: { select: { note: true, noteStage: true } },
+ reviews: {
+ select: {
+ id: true,
+ content: true,
+ author: { select: authorSelect },
+ },
+ orderBy: { createdAt: "desc" as const },
+ take: 5,
+ },
+ _count: {
+ select: { bookmarks: true, reviews: true, collectedByUsers: true },
+ },
+} satisfies Prisma.PerfumeInclude;
+
+export type BasePerfume = Prisma.PerfumeGetPayload<{
+ include: typeof perfumeBaseInclude;
+}>;
+
+export type FullPerfume = Prisma.PerfumeGetPayload<{
+ include: typeof perfumeDetailInclude;
+}>;
diff --git a/src/server/hono/repositories/review.repository.ts b/src/server/hono/repositories/review.repository.ts
new file mode 100644
index 00000000..dd7b71db
--- /dev/null
+++ b/src/server/hono/repositories/review.repository.ts
@@ -0,0 +1,30 @@
+import { Prisma } from "@prisma/client";
+import { authorSelect } from "./user.repository";
+import { perfumeBaseInclude } from "./perfume.repository";
+
+export const reviewIncludeArgs = {
+ author: {
+ select: authorSelect,
+ },
+ perfume: {
+ select: {
+ id: true,
+ nameKo: true,
+ nameEn: true,
+ ...perfumeBaseInclude,
+ },
+ },
+ attributeSelections: {
+ include: {
+ option: {
+ include: {
+ attribute: true,
+ },
+ },
+ },
+ },
+} satisfies Prisma.ReviewInclude;
+
+export type FullReview = Prisma.ReviewGetPayload<{
+ include: typeof reviewIncludeArgs;
+}>;
diff --git a/src/server/hono/repositories/user.repository.ts b/src/server/hono/repositories/user.repository.ts
new file mode 100644
index 00000000..112fcefd
--- /dev/null
+++ b/src/server/hono/repositories/user.repository.ts
@@ -0,0 +1,31 @@
+import { Prisma } from "@prisma/client";
+
+export const authorSelect = {
+ id: true,
+ nickname: true,
+ imageUrl: true,
+} satisfies Prisma.UserSelect;
+
+export const myCollectionInclude = {
+ image: true,
+} satisfies Prisma.UserCollectionInclude;
+
+export const myCommentInclude = {
+ post: { select: { id: true, title: true } },
+} satisfies Prisma.CommentInclude;
+
+export type UserProfile = Prisma.UserGetPayload<{
+ select: typeof authorSelect;
+}>;
+
+export type MyCollection = Prisma.UserCollectionGetPayload<{
+ include: typeof myCollectionInclude;
+}>;
+
+export type MyComment = Prisma.CommentGetPayload<{
+ include: typeof myCommentInclude;
+}>;
+
+export type UserCollectionWithRelations = Prisma.UserCollectionGetPayload<{
+ include: { image: true; perfume: true };
+}>;
diff --git a/src/server/hono/routes/v1/auth.handler.ts b/src/server/hono/routes/v1/auth.handler.ts
new file mode 100644
index 00000000..09d68f44
--- /dev/null
+++ b/src/server/hono/routes/v1/auth.handler.ts
@@ -0,0 +1,142 @@
+import { createRoute, z } from "@hono/zod-openapi";
+import { OpenAPIHono } from "@hono/zod-openapi";
+import type { Context, Next } from "hono";
+import {
+ syncOAuthUserService,
+ confirmOAuthLinkService,
+ getSessionUserService,
+} from "@/server/hono/services/auth.service";
+import {
+ apiBadRequest,
+ apiForbidden,
+ apiInternalError,
+ apiNotFound,
+ apiSuccess,
+} from "@/server/hono/utils/api.utils";
+import { createStandardApiResponses } from "@/server/hono/utils/openapi.schema";
+import type { AppContext } from "@/server/hono/app";
+
+const router = new OpenAPIHono();
+
+const internalAuth = async (c: Context, next: Next) => {
+ const secret = c.req.header("x-internal-secret");
+ if (!secret || secret !== process.env.INTERNAL_API_SECRET) {
+ return apiForbidden(c, "์ ๊ทผ์ด ๊ฑฐ๋ถ๋์์ต๋๋ค.");
+ }
+ await next();
+};
+
+// โโ ๋ด๋ถ ์ ์ฉ ๋ผ์ฐํธ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+
+const SyncOAuthUserBodySchema = z.object({
+ provider: z.string().min(1),
+ providerAccountId: z.string().min(1),
+ email: z.string().email(),
+});
+
+const SyncOAuthUserResponseSchema = z.discriminatedUnion("type", [
+ z.object({ type: z.literal("success"), id: z.string(), isNewUser: z.boolean() }),
+ z.object({ type: z.literal("email_conflict"), token: z.string() }),
+]);
+
+const syncOAuthUserRoute = createRoute({
+ method: "post",
+ path: "/sync",
+ summary: "[๋ด๋ถ] OAuth ์ ์ ๋๊ธฐํ",
+ request: { body: { content: { "application/json": { schema: SyncOAuthUserBodySchema } } } },
+ responses: createStandardApiResponses({ schema: SyncOAuthUserResponseSchema }),
+ tags: ["Auth (Internal)"],
+});
+
+router.use("/sync", internalAuth);
+router.openapi(syncOAuthUserRoute, async (c) => {
+ const body = c.req.valid("json");
+ const result = await syncOAuthUserService(body);
+
+ if (!result.success) {
+ switch (result.error) {
+ case "BAD_REQUEST":
+ return apiBadRequest(c, result.message);
+ default:
+ return apiInternalError(c, result.message);
+ }
+ }
+
+ return apiSuccess(c, result.data, "์ ์ ๋๊ธฐํ๊ฐ ์๋ฃ๋์์ต๋๋ค.");
+});
+
+const SessionUserParamSchema = z.object({
+ userId: z.string().uuid(),
+});
+
+const SessionUserResponseSchema = z.object({
+ id: z.string(),
+ isActive: z.boolean(),
+ nickname: z.string(),
+ imageUrl: z.string().nullable(),
+});
+
+const getSessionUserRoute = createRoute({
+ method: "get",
+ path: "/session-user/{userId}",
+ summary: "[๋ด๋ถ] ์ธ์
์ ์ ์ต์ ์ ๋ณด ์กฐํ",
+ request: { params: SessionUserParamSchema },
+ responses: createStandardApiResponses({ schema: SessionUserResponseSchema }),
+ tags: ["Auth (Internal)"],
+});
+
+router.use("/session-user/*", internalAuth);
+router.openapi(getSessionUserRoute, async (c) => {
+ const { userId } = c.req.valid("param");
+ const result = await getSessionUserService(userId);
+
+ if (!result.success) {
+ switch (result.error) {
+ case "NOT_FOUND":
+ return apiNotFound(c, result.message);
+ default:
+ return apiInternalError(c, result.message);
+ }
+ }
+
+ return apiSuccess(c, result.data, "์ธ์
์ ์ ์ ๋ณด๋ฅผ ์ฑ๊ณต์ ์ผ๋ก ๋ถ๋ฌ์์ต๋๋ค.");
+});
+
+// โโ ๊ณต๊ฐ ๋ผ์ฐํธ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+
+const ConfirmLinkBodySchema = z.object({
+ token: z.string().uuid(),
+});
+
+const ConfirmLinkResponseSchema = z.object({
+ userId: z.string(),
+});
+
+const confirmLinkRoute = createRoute({
+ method: "post",
+ path: "/confirm-link",
+ summary: "OAuth ๊ณ์ ์ฐ๊ฒฐ ํ์ธ",
+ request: { body: { content: { "application/json": { schema: ConfirmLinkBodySchema } } } },
+ responses: createStandardApiResponses({ schema: ConfirmLinkResponseSchema }),
+ tags: ["Auth"],
+});
+
+router.openapi(confirmLinkRoute, async (c) => {
+ const { token } = c.req.valid("json");
+ const result = await confirmOAuthLinkService(token);
+
+ if (!result.success) {
+ switch (result.error) {
+ case "NOT_FOUND":
+ return apiNotFound(c, result.message);
+ case "BAD_REQUEST":
+ return apiBadRequest(c, result.message);
+ default:
+ return apiInternalError(c, result.message);
+ }
+ }
+
+ return apiSuccess(c, result.data, "๊ณ์ ์ฐ๊ฒฐ์ด ์๋ฃ๋์์ต๋๋ค.");
+});
+
+export default router;
diff --git a/src/lib/hono/routes/v1/brand.handler.ts b/src/server/hono/routes/v1/brand.handler.ts
similarity index 67%
rename from src/lib/hono/routes/v1/brand.handler.ts
rename to src/server/hono/routes/v1/brand.handler.ts
index 5139481d..8819af8c 100644
--- a/src/lib/hono/routes/v1/brand.handler.ts
+++ b/src/server/hono/routes/v1/brand.handler.ts
@@ -1,16 +1,17 @@
import { createRoute } from "@hono/zod-openapi";
import { z } from "@hono/zod-openapi";
-import * as BrandSchemas from "@/lib/hono/schemas/brand.schema";
-import * as BrandServices from "@/lib/hono/services/brand.service";
+import * as BrandSchemas from "@/server/hono/schemas/brand.schema";
+import * as BrandServices from "@/server/hono/services/brand.service";
import {
apiInternalError,
apiNotFound,
apiSuccess,
-} from "@/lib/hono/utils/api.utils";
-import { createStandardApiResponses } from "@/lib/hono/utils/openapi.schema";
-import { createDomainRouters } from "@/lib/hono/utils/router";
+} from "@/server/hono/utils/api.utils";
+import { createStandardApiResponses } from "@/server/hono/utils/openapi.schema";
+import { createDomainRouters } from "@/server/hono/utils/router";
const routers = createDomainRouters();
+const publicRouter = routers.public;
/**
* @method GET
@@ -104,4 +105,44 @@ routers.optionalAuth.openapi(getBrandByNameRoute, async (c) => {
return apiSuccess(c, result.data, "๋ธ๋๋ ์ ๋ณด๋ฅผ ์ฑ๊ณต์ ์ผ๋ก ๋ถ๋ฌ์์ต๋๋ค.");
});
+/**
+ * @method GET
+ * @path /stores/:name
+ * @description ์นด์นด์ค ๋ก์ปฌ ๊ฒ์ API๋ฅผ ํตํด ๋ธ๋๋ ๋งค์ฅ ๋ชฉ๋ก์ ์กฐํํฉ๋๋ค.
+ * @summary ๋ธ๋๋ ๋งค์ฅ ๊ฒ์
+ */
+publicRouter.get("/stores/:name", async (c) => {
+ const name = c.req.param("name");
+ const { x, y } = c.req.query();
+
+ const result = await BrandServices.getStoresByNameService(name, x, y);
+
+ if (!result.success) {
+ return apiInternalError(c, result.message);
+ }
+
+ return c.json(
+ { success: true, stores: result.data.stores, total: result.data.total },
+ 200
+ );
+});
+
+/**
+ * @method GET
+ * @path /location
+ * @description ์ขํ๋ฅผ ์ง์ญ ์ ๋ณด๋ก ๋ณํํฉ๋๋ค.
+ * @summary ์ขํ โ ์ง์ญ ์ ๋ณด ๋ณํ
+ */
+publicRouter.get("/location", async (c) => {
+ const { x, y } = c.req.query();
+
+ const result = await BrandServices.getRegionByCoordService(x, y);
+
+ if (!result.success) {
+ return apiInternalError(c, result.message);
+ }
+
+ return c.json({ success: true, region: result.data }, 200);
+});
+
export default routers.merge();
diff --git a/src/lib/hono/routes/v1/comment.handler.ts b/src/server/hono/routes/v1/comment.handler.ts
similarity index 91%
rename from src/lib/hono/routes/v1/comment.handler.ts
rename to src/server/hono/routes/v1/comment.handler.ts
index de02300f..ed4eefbe 100644
--- a/src/lib/hono/routes/v1/comment.handler.ts
+++ b/src/server/hono/routes/v1/comment.handler.ts
@@ -1,6 +1,6 @@
import { createRoute } from "@hono/zod-openapi";
-import * as CommentSchemas from "@/lib/hono/schemas/comment.schema";
-import * as CommentServices from "@/lib/hono/services/comment.service";
+import * as CommentSchemas from "@/server/hono/schemas/comment.schema";
+import * as CommentServices from "@/server/hono/services/comment.service";
import {
apiInternalError,
apiSuccess,
@@ -9,10 +9,10 @@ import {
apiBadRequest,
apiCreated,
apiForbidden,
-} from "@/lib/hono/utils/api.utils";
-import { createStandardApiResponses } from "@/lib/hono/utils/openapi.schema";
-import { createDomainRouters } from "@/lib/hono/utils/router";
-import { getAuthenticatedUser } from "@/lib/hono/utils/service.utils";
+} from "@/server/hono/utils/api.utils";
+import { createStandardApiResponses } from "@/server/hono/utils/openapi.schema";
+import { createDomainRouters } from "@/server/hono/utils/router";
+
const routers = createDomainRouters();
@@ -127,7 +127,8 @@ const createCommentRoute = createRoute({
routers.authenticated.openapi(createCommentRoute, async (c) => {
const { postId } = c.req.valid("param");
const body = c.req.valid("json");
- const user = getAuthenticatedUser(c);
+ const user = c.get("user");
+ if (!user?.id) throw new Error("์ธ์ฆ๋์ง ์์ ์ฌ์ฉ์์
๋๋ค.");
const payload: CommentSchemas.CreateCommentPayload = {
...body,
@@ -188,7 +189,8 @@ const editCommentRoute = createRoute({
routers.authenticated.openapi(editCommentRoute, async (c) => {
const { commentId } = c.req.valid("param");
const body = c.req.valid("json");
- const user = getAuthenticatedUser(c);
+ const user = c.get("user");
+ if (!user?.id) throw new Error("์ธ์ฆ๋์ง ์์ ์ฌ์ฉ์์
๋๋ค.");
const payload = {
id: commentId,
authorId: user.id,
@@ -231,7 +233,8 @@ const deleteCommentRoute = createRoute({
routers.authenticated.openapi(deleteCommentRoute, async (c) => {
const { commentId } = c.req.valid("param");
- const user = getAuthenticatedUser(c);
+ const user = c.get("user");
+ if (!user?.id) throw new Error("์ธ์ฆ๋์ง ์์ ์ฌ์ฉ์์
๋๋ค.");
const payload: CommentSchemas.DeleteCommentPayload = {
id: commentId,
authorId: user.id,
diff --git a/src/lib/hono/routes/v1/community.handler.ts b/src/server/hono/routes/v1/community.handler.ts
similarity index 93%
rename from src/lib/hono/routes/v1/community.handler.ts
rename to src/server/hono/routes/v1/community.handler.ts
index 37297509..2c175b66 100644
--- a/src/lib/hono/routes/v1/community.handler.ts
+++ b/src/server/hono/routes/v1/community.handler.ts
@@ -1,7 +1,7 @@
import { createRoute, z } from "@hono/zod-openapi";
import { PostCategory } from "@prisma/client";
-import * as CommunitySchemas from "@/lib/hono/schemas/community.schema";
-import * as CommunityServices from "@/lib/hono/services/community.service";
+import * as CommunitySchemas from "@/server/hono/schemas/community.schema";
+import * as CommunityServices from "@/server/hono/services/community";
import {
apiBadRequest,
apiNotFound,
@@ -9,10 +9,10 @@ import {
apiInternalError,
apiCreated,
apiForbidden,
-} from "@/lib/hono/utils/api.utils";
-import { createStandardApiResponses } from "@/lib/hono/utils/openapi.schema";
-import { createDomainRouters } from "@/lib/hono/utils/router";
-import { getAuthenticatedUser } from "@/lib/hono/utils/service.utils";
+} from "@/server/hono/utils/api.utils";
+import { createStandardApiResponses } from "@/server/hono/utils/openapi.schema";
+import { createDomainRouters } from "@/server/hono/utils/router";
+
const routers = createDomainRouters();
@@ -198,7 +198,8 @@ const createPostRoute = createRoute({
});
routers.authenticated.openapi(createPostRoute, async (c) => {
- const user = getAuthenticatedUser(c);
+ const user = c.get("user");
+ if (!user?.id) throw new Error("์ธ์ฆ๋์ง ์์ ์ฌ์ฉ์์
๋๋ค.");
const body = c.req.valid("json");
const result = await CommunityServices.createPostService({
@@ -242,7 +243,8 @@ const editPostRoute = createRoute({
routers.authenticated.openapi(editPostRoute, async (c) => {
const { id } = c.req.valid("param");
- const user = getAuthenticatedUser(c);
+ const user = c.get("user");
+ if (!user?.id) throw new Error("์ธ์ฆ๋์ง ์์ ์ฌ์ฉ์์
๋๋ค.");
const result = await CommunityServices.updatePostService(
id,
@@ -287,7 +289,8 @@ const deletePostRoute = createRoute({
routers.authenticated.openapi(deletePostRoute, async (c) => {
const { id } = c.req.valid("param");
- const user = getAuthenticatedUser(c);
+ const user = c.get("user");
+ if (!user?.id) throw new Error("์ธ์ฆ๋์ง ์์ ์ฌ์ฉ์์
๋๋ค.");
const result = await CommunityServices.deletePostService(id, user.id);
if (!result.success) {
@@ -322,7 +325,8 @@ const likePostRoute = createRoute({
routers.authenticated.openapi(likePostRoute, async (c) => {
const { id } = c.req.valid("param");
- const user = getAuthenticatedUser(c);
+ const user = c.get("user");
+ if (!user?.id) throw new Error("์ธ์ฆ๋์ง ์์ ์ฌ์ฉ์์
๋๋ค.");
const result = await CommunityServices.togglePostLikeService(id, user.id);
if (!result.success) {
@@ -357,7 +361,8 @@ const bookmarkPostRoute = createRoute({
routers.authenticated.openapi(bookmarkPostRoute, async (c) => {
const { id } = c.req.valid("param");
- const user = getAuthenticatedUser(c);
+ const user = c.get("user");
+ if (!user?.id) throw new Error("์ธ์ฆ๋์ง ์์ ์ฌ์ฉ์์
๋๋ค.");
const result = await CommunityServices.togglePostBookmarkService(id, user.id);
if (!result.success) {
diff --git a/src/lib/hono/routes/v1/draft.handler.ts b/src/server/hono/routes/v1/draft.handler.ts
similarity index 72%
rename from src/lib/hono/routes/v1/draft.handler.ts
rename to src/server/hono/routes/v1/draft.handler.ts
index f91a81f3..8fb43d4d 100644
--- a/src/lib/hono/routes/v1/draft.handler.ts
+++ b/src/server/hono/routes/v1/draft.handler.ts
@@ -1,7 +1,6 @@
-import { createRoute } from "@hono/zod-openapi";
-import { z } from "zod";
-import * as DraftSchemas from "@/lib/hono/schemas/draft.schema";
-import * as DraftServices from "@/lib/hono/services/draft.service";
+import { createRoute, z } from "@hono/zod-openapi";
+import * as DraftSchemas from "@/server/hono/schemas/draft.schema";
+import * as DraftServices from "@/server/hono/services/draft.service";
import {
apiBadRequest,
apiCreated,
@@ -9,10 +8,10 @@ import {
apiInternalError,
apiNotFound,
apiSuccess,
-} from "@/lib/hono/utils/api.utils";
-import { createStandardApiResponses } from "@/lib/hono/utils/openapi.schema";
-import { createDomainRouters } from "@/lib/hono/utils/router";
-import { getAuthenticatedUser } from "@/lib/hono/utils/service.utils";
+} from "@/server/hono/utils/api.utils";
+import { createStandardApiResponses } from "@/server/hono/utils/openapi.schema";
+import { createDomainRouters } from "@/server/hono/utils/router";
+
const routers = createDomainRouters();
@@ -43,7 +42,8 @@ const createOrUpdateDraftRoute = createRoute({
});
routers.authenticated.openapi(createOrUpdateDraftRoute, async (c) => {
- const user = getAuthenticatedUser(c);
+ const user = c.get("user");
+ if (!user?.id) throw new Error("์ธ์ฆ๋์ง ์์ ์ฌ์ฉ์์
๋๋ค.");
const body = c.req.valid("json");
const payload: DraftSchemas.CreateDraftPayload = {
@@ -66,16 +66,19 @@ routers.authenticated.openapi(createOrUpdateDraftRoute, async (c) => {
return apiCreated(c, result.data, "์์ ์ ์ฅ์ ์ฑ๊ณต์ ์ผ๋ก ์ ์ฅํ์ต๋๋ค.");
});
-// ========== GET /drafts - ์์ ์ ์ฅ ๋ชฉ๋ก ์กฐํ ==========
+// ========== GET /drafts - ์์ ์ ์ฅ ์กฐํ ==========
const listDraftsRoute = createRoute({
method: "get",
path: "/",
- summary: "๋ด ์์ ์ ์ฅ ๋ชฉ๋ก ์กฐํ",
- description: "ํ์ฌ ์ฌ์ฉ์์ ๋ชจ๋ ์์ ์ ์ฅ ๋ชฉ๋ก์ ์กฐํํฉ๋๋ค.",
+ summary: "๋ด ์์ ์ ์ฅ ์กฐํ",
+ description: "type์ ์ง์ ํ๋ฉด ํด๋น ํ์
์ ๋จ์ผ ์์ ์ ์ฅ์, ์๋ตํ๋ฉด ์ ์ฒด ๋ชฉ๋ก์ ๋ฐํํฉ๋๋ค.",
+ request: {
+ query: DraftSchemas.ListDraftQuerySchema,
+ },
responses: createStandardApiResponses(
{
- schema: DraftSchemas.ApiDraftListResponseSchema,
- description: "์์ ์ ์ฅ ๋ชฉ๋ก",
+ schema: z.union([DraftSchemas.ApiDraftResponseSchema, DraftSchemas.ApiDraftListResponseSchema]),
+ description: "์์ ์ ์ฅ ์กฐํ ๊ฒฐ๊ณผ",
},
["401"]
),
@@ -83,12 +86,16 @@ const listDraftsRoute = createRoute({
});
routers.authenticated.openapi(listDraftsRoute, async (c) => {
- const user = getAuthenticatedUser(c);
+ const user = c.get("user");
+ if (!user?.id) throw new Error("์ธ์ฆ๋์ง ์์ ์ฌ์ฉ์์
๋๋ค.");
- const result = await DraftServices.listDraftsService(user.id);
+ const { type } = c.req.valid("query");
- if (!result.success) {
- return apiInternalError(c, result.message);
+ const result = await DraftServices.listDraftsService(user.id, type);
+ if (!result.success) return apiInternalError(c, result.message);
+
+ if (type) {
+ return apiSuccess(c, result.data ?? null, "์์ ์ ์ฅ์ ์ฑ๊ณต์ ์ผ๋ก ์กฐํํ์ต๋๋ค.");
}
return apiSuccess(c, result.data, "์์ ์ ์ฅ ๋ชฉ๋ก์ ์ฑ๊ณต์ ์ผ๋ก ์กฐํํ์ต๋๋ค.");
@@ -115,7 +122,8 @@ const getDraftRoute = createRoute({
routers.authenticated.openapi(getDraftRoute, async (c) => {
const { id } = c.req.valid("param");
- const user = getAuthenticatedUser(c);
+ const user = c.get("user");
+ if (!user?.id) throw new Error("์ธ์ฆ๋์ง ์์ ์ฌ์ฉ์์
๋๋ค.");
const result = await DraftServices.getDraftService(id, user.id);
@@ -153,7 +161,8 @@ const deleteDraftRoute = createRoute({
routers.authenticated.openapi(deleteDraftRoute, async (c) => {
const { id } = c.req.valid("param");
- const user = getAuthenticatedUser(c);
+ const user = c.get("user");
+ if (!user?.id) throw new Error("์ธ์ฆ๋์ง ์์ ์ฌ์ฉ์์
๋๋ค.");
const result = await DraftServices.deleteDraftService(id, user.id);
diff --git a/src/lib/hono/routes/v1/file.handler.ts b/src/server/hono/routes/v1/file.handler.ts
similarity index 77%
rename from src/lib/hono/routes/v1/file.handler.ts
rename to src/server/hono/routes/v1/file.handler.ts
index 93126840..8b92d041 100644
--- a/src/lib/hono/routes/v1/file.handler.ts
+++ b/src/server/hono/routes/v1/file.handler.ts
@@ -1,14 +1,14 @@
import { createRoute } from "@hono/zod-openapi";
-import { UploadedImageInfoSchema } from "@/lib/hono/schemas/common.schema";
-import * as FileServices from "@/lib/hono/services/file.service";
+import { UploadedImageInfoSchema } from "@/server/hono/schemas/common.schema";
+import * as FileServices from "@/server/hono/services/file.service";
import {
apiBadRequest,
apiInternalError,
apiSuccess,
-} from "@/lib/hono/utils/api.utils";
-import { createStandardApiResponses } from "@/lib/hono/utils/openapi.schema";
-import { createDomainRouters } from "@/lib/hono/utils/router";
-import { getAuthenticatedUser } from "@/lib/hono/utils/service.utils";
+} from "@/server/hono/utils/api.utils";
+import { createStandardApiResponses } from "@/server/hono/utils/openapi.schema";
+import { createDomainRouters } from "@/server/hono/utils/router";
+
const routers = createDomainRouters();
@@ -43,7 +43,8 @@ const uploadRoute = createRoute({
});
routers.authenticated.openapi(uploadRoute, async (c) => {
- const user = getAuthenticatedUser(c);
+ const user = c.get("user");
+ if (!user?.id) throw new Error("์ธ์ฆ๋์ง ์์ ์ฌ์ฉ์์
๋๋ค.");
const formData = await c.req.formData();
const file = formData.get("file");
const bucketName = formData.get("bucketName");
diff --git a/src/lib/hono/routes/v1/filter.handler.ts b/src/server/hono/routes/v1/filter.handler.ts
similarity index 84%
rename from src/lib/hono/routes/v1/filter.handler.ts
rename to src/server/hono/routes/v1/filter.handler.ts
index b73c1e3f..dcd8092f 100644
--- a/src/lib/hono/routes/v1/filter.handler.ts
+++ b/src/server/hono/routes/v1/filter.handler.ts
@@ -1,12 +1,12 @@
import { createRoute } from "@hono/zod-openapi";
-import * as FilterSchemas from "@/lib/hono/schemas/filter.schema";
+import * as FilterSchemas from "@/server/hono/schemas/filter.schema";
import {
getAvailableFiltersService,
getAvailableFiltersTotalService,
-} from "@/lib/hono/services/filter.service";
-import { apiInternalError, apiSuccess } from "@/lib/hono/utils/api.utils";
-import { createStandardApiResponses } from "@/lib/hono/utils/openapi.schema";
-import { createDomainRouters } from "@/lib/hono/utils/router";
+} from "@/server/hono/services/filter.service";
+import { apiInternalError, apiSuccess } from "@/server/hono/utils/api.utils";
+import { createStandardApiResponses } from "@/server/hono/utils/openapi.schema";
+import { createDomainRouters } from "@/server/hono/utils/router";
const routers = createDomainRouters();
diff --git a/src/lib/hono/routes/v1/me.handler.ts b/src/server/hono/routes/v1/me.handler.ts
similarity index 81%
rename from src/lib/hono/routes/v1/me.handler.ts
rename to src/server/hono/routes/v1/me.handler.ts
index 272df72b..940163fb 100644
--- a/src/lib/hono/routes/v1/me.handler.ts
+++ b/src/server/hono/routes/v1/me.handler.ts
@@ -1,16 +1,16 @@
import { createRoute, z } from "@hono/zod-openapi";
-import * as MeSchemas from "@/lib/hono/schemas/me.schema";
-import * as MeServices from "@/lib/hono/services/me.service";
+import * as MeSchemas from "@/server/hono/schemas/me.schema";
+import * as MeServices from "@/server/hono/services/me";
import {
apiBadRequest,
apiForbidden,
apiInternalError,
apiNotFound,
apiSuccess,
-} from "@/lib/hono/utils/api.utils";
-import { createStandardApiResponses } from "@/lib/hono/utils/openapi.schema";
-import { createDomainRouters } from "@/lib/hono/utils/router";
-import { getAuthenticatedUser } from "@/lib/hono/utils/service.utils";
+} from "@/server/hono/utils/api.utils";
+import { createStandardApiResponses } from "@/server/hono/utils/openapi.schema";
+import { createDomainRouters } from "@/server/hono/utils/router";
+
const routers = createDomainRouters();
@@ -32,7 +32,8 @@ const getMyBookmarkedPostsRoute = createRoute({
});
routers.authenticated.openapi(getMyBookmarkedPostsRoute, async (c) => {
- const user = getAuthenticatedUser(c);
+ const user = c.get("user");
+ if (!user?.id) throw new Error("์ธ์ฆ๋์ง ์์ ์ฌ์ฉ์์
๋๋ค.");
const result = await MeServices.getMyBookmarkedPostsService(user.id);
if (!result.success) {
@@ -61,7 +62,8 @@ const getMyBookmarkedPerfumesRoute = createRoute({
});
routers.authenticated.openapi(getMyBookmarkedPerfumesRoute, async (c) => {
- const user = getAuthenticatedUser(c);
+ const user = c.get("user");
+ if (!user?.id) throw new Error("์ธ์ฆ๋์ง ์์ ์ฌ์ฉ์์
๋๋ค.");
const result = await MeServices.getMyBookmarkedPerfumesService(user.id);
@@ -100,7 +102,8 @@ const postPhotoCollectionRoute = createRoute({
});
routers.authenticated.openapi(postPhotoCollectionRoute, async (c) => {
- const user = getAuthenticatedUser(c);
+ const user = c.get("user");
+ if (!user?.id) throw new Error("์ธ์ฆ๋์ง ์์ ์ฌ์ฉ์์
๋๋ค.");
const body = c.req.valid("json");
const data = {
@@ -140,7 +143,8 @@ const deletePhotoCollectionRoute = createRoute({
});
routers.authenticated.openapi(deletePhotoCollectionRoute, async (c) => {
- const user = getAuthenticatedUser(c);
+ const user = c.get("user");
+ if (!user?.id) throw new Error("์ธ์ฆ๋์ง ์์ ์ฌ์ฉ์์
๋๋ค.");
const { collectionId } = c.req.valid("param");
const result = await MeServices.deletePhotoCollectionService({
@@ -183,7 +187,8 @@ const getMyReviewsRoute = createRoute({
});
routers.authenticated.openapi(getMyReviewsRoute, async (c) => {
- const user = getAuthenticatedUser(c);
+ const user = c.get("user");
+ if (!user?.id) throw new Error("์ธ์ฆ๋์ง ์์ ์ฌ์ฉ์์
๋๋ค.");
const options = c.req.valid("query");
const result = await MeServices.getMyReviewsService(user.id, options);
@@ -220,7 +225,8 @@ const getMyPostsRoute = createRoute({
});
routers.authenticated.openapi(getMyPostsRoute, async (c) => {
- const user = getAuthenticatedUser(c);
+ const user = c.get("user");
+ if (!user?.id) throw new Error("์ธ์ฆ๋์ง ์์ ์ฌ์ฉ์์
๋๋ค.");
const options = c.req.valid("query");
const result = await MeServices.getMyPostsService(user.id, options);
@@ -257,7 +263,8 @@ const getMyCommentsRoute = createRoute({
});
routers.authenticated.openapi(getMyCommentsRoute, async (c) => {
- const user = getAuthenticatedUser(c);
+ const user = c.get("user");
+ if (!user?.id) throw new Error("์ธ์ฆ๋์ง ์์ ์ฌ์ฉ์์
๋๋ค.");
const options = c.req.valid("query");
const result = await MeServices.getMyCommentsService(user.id, options);
@@ -288,7 +295,8 @@ const getMyLikedPerfumesRoute = createRoute({
});
routers.authenticated.openapi(getMyLikedPerfumesRoute, async (c) => {
- const user = getAuthenticatedUser(c);
+ const user = c.get("user");
+ if (!user?.id) throw new Error("์ธ์ฆ๋์ง ์์ ์ฌ์ฉ์์
๋๋ค.");
const result = await MeServices.getMyLikedPerfumesService(user.id);
@@ -318,7 +326,8 @@ const getMyLikedPostsRoute = createRoute({
});
routers.authenticated.openapi(getMyLikedPostsRoute, async (c) => {
- const user = getAuthenticatedUser(c);
+ const user = c.get("user");
+ if (!user?.id) throw new Error("์ธ์ฆ๋์ง ์์ ์ฌ์ฉ์์
๋๋ค.");
const result = await MeServices.getMyLikedPostsService(user.id);
@@ -348,7 +357,8 @@ const getMyProfileRoute = createRoute({
});
routers.authenticated.openapi(getMyProfileRoute, async (c) => {
- const user = getAuthenticatedUser(c);
+ const user = c.get("user");
+ if (!user?.id) throw new Error("์ธ์ฆ๋์ง ์์ ์ฌ์ฉ์์
๋๋ค.");
const result = await MeServices.getMyProfileService(user.id);
@@ -386,7 +396,8 @@ const patchMyProfileRoute = createRoute({
});
routers.authenticated.openapi(patchMyProfileRoute, async (c) => {
- const user = getAuthenticatedUser(c);
+ const user = c.get("user");
+ if (!user?.id) throw new Error("์ธ์ฆ๋์ง ์์ ์ฌ์ฉ์์
๋๋ค.");
const formData = c.req.valid("json");
const result = await MeServices.updateMyProfileService({
id: user.id,
@@ -394,6 +405,7 @@ routers.authenticated.openapi(patchMyProfileRoute, async (c) => {
});
if (!result.success) {
+ if (result.error === "BAD_REQUEST") return apiBadRequest(c, result.message);
return apiInternalError(c, result.message);
}
return apiSuccess(c, result.data, "๋ด ์ ๋ณด๋ฅผ ์ฑ๊ณต์ ์ผ๋ก ์์ ํ์ต๋๋ค.");
@@ -427,7 +439,8 @@ const postRecentPerfumesRoute = createRoute({
});
routers.authenticated.openapi(postRecentPerfumesRoute, async (c) => {
- const user = getAuthenticatedUser(c);
+ const user = c.get("user");
+ if (!user?.id) throw new Error("์ธ์ฆ๋์ง ์์ ์ฌ์ฉ์์
๋๋ค.");
const formData = c.req.valid("json");
// DB์ ์ต๊ทผ ๋ณธ ํฅ์ ๊ธฐ๋ก ๋๊ธฐํ
const result = await MeServices.syncRecentPerfumesService({
@@ -460,7 +473,8 @@ const getRecentPerfumesRoute = createRoute({
});
routers.authenticated.openapi(getRecentPerfumesRoute, async (c) => {
- const user = getAuthenticatedUser(c);
+ const user = c.get("user");
+ if (!user?.id) throw new Error("์ธ์ฆ๋์ง ์์ ์ฌ์ฉ์์
๋๋ค.");
const result = await MeServices.getRecentPerfumesService(user.id);
if (!result.success) {
return apiInternalError(c, result.message);
@@ -500,7 +514,8 @@ const postRecentPostsRoute = createRoute({
});
routers.authenticated.openapi(postRecentPostsRoute, async (c) => {
- const user = getAuthenticatedUser(c);
+ const user = c.get("user");
+ if (!user?.id) throw new Error("์ธ์ฆ๋์ง ์์ ์ฌ์ฉ์์
๋๋ค.");
const formData = c.req.valid("json");
const result = await MeServices.syncRecentPostsService({
userId: user.id,
@@ -532,7 +547,8 @@ const getRecentPostsRoute = createRoute({
});
routers.authenticated.openapi(getRecentPostsRoute, async (c) => {
- const user = getAuthenticatedUser(c);
+ const user = c.get("user");
+ if (!user?.id) throw new Error("์ธ์ฆ๋์ง ์์ ์ฌ์ฉ์์
๋๋ค.");
const result = await MeServices.getRecentPostsService(user.id);
if (!result.success) {
return apiInternalError(c, result.message);
@@ -544,4 +560,38 @@ routers.authenticated.openapi(getRecentPostsRoute, async (c) => {
);
});
+/**
+ * @method delete
+ * @path /me
+ * @summary ํ์ ํํด
+ */
+const deleteMyAccountRoute = createRoute({
+ method: "delete",
+ path: "/",
+ summary: "ํ์ ํํด",
+ responses: createStandardApiResponses({
+ schema: z.object({ message: z.string() }),
+ }),
+ tags: ["Me"],
+});
+
+routers.authenticated.openapi(deleteMyAccountRoute, async (c) => {
+ const user = c.get("user");
+ if (!user?.id) throw new Error("์ธ์ฆ๋์ง ์์ ์ฌ์ฉ์์
๋๋ค.");
+
+ const result = await MeServices.deleteMyAccountService(user.id);
+
+ if (!result.success) {
+ switch (result.error) {
+ case "NOT_FOUND":
+ return apiNotFound(c, result.message);
+ case "BAD_REQUEST":
+ return apiBadRequest(c, result.message);
+ default:
+ return apiInternalError(c, result.message);
+ }
+ }
+ return apiSuccess(c, result.data, result.data.message);
+});
+
export default routers.merge();
diff --git a/src/lib/hono/routes/v1/perfume.handler.ts b/src/server/hono/routes/v1/perfume.handler.ts
similarity index 91%
rename from src/lib/hono/routes/v1/perfume.handler.ts
rename to src/server/hono/routes/v1/perfume.handler.ts
index e5c40997..e1409d39 100644
--- a/src/lib/hono/routes/v1/perfume.handler.ts
+++ b/src/server/hono/routes/v1/perfume.handler.ts
@@ -1,19 +1,19 @@
import { createRoute, z } from "@hono/zod-openapi";
-import { ApiPostResponseSchema } from "@/lib/hono/schemas/community.schema";
-import * as CommonSchemas from "@/lib/hono/schemas/common.schema";
-import * as PerfumeSchemas from "@/lib/hono/schemas/perfume.schema";
+import { ApiPostResponseSchema } from "@/server/hono/schemas/community.schema";
+import * as CommonSchemas from "@/server/hono/schemas/common.schema";
+import * as PerfumeSchemas from "@/server/hono/schemas/perfume.schema";
-import * as PerfumeServices from "@/lib/hono/services/perfume.service";
+import * as PerfumeServices from "@/server/hono/services/perfume.service";
import {
apiInternalError,
apiNotFound,
apiSuccess,
-} from "@/lib/hono/utils/api.utils";
-import { createStandardApiResponses } from "@/lib/hono/utils/openapi.schema";
-import { createDomainRouters } from "@/lib/hono/utils/router";
-import { getAuthenticatedUser } from "@/lib/hono/utils/service.utils";
+} from "@/server/hono/utils/api.utils";
+import { createStandardApiResponses } from "@/server/hono/utils/openapi.schema";
+import { createDomainRouters } from "@/server/hono/utils/router";
+
const routers = createDomainRouters();
@@ -212,7 +212,8 @@ const toggleBookmarkRoute = createRoute({
routers.authenticated.openapi(toggleBookmarkRoute, async (c) => {
const { id } = c.req.valid("param");
- const user = getAuthenticatedUser(c);
+ const user = c.get("user");
+ if (!user?.id) throw new Error("์ธ์ฆ๋์ง ์์ ์ฌ์ฉ์์
๋๋ค.");
const result = await PerfumeServices.togglePerfumeBookmarkService(
id,
user.id
@@ -250,7 +251,8 @@ const toggleLikeRoute = createRoute({
routers.authenticated.openapi(toggleLikeRoute, async (c) => {
const { id } = c.req.valid("param");
- const user = getAuthenticatedUser(c);
+ const user = c.get("user");
+ if (!user?.id) throw new Error("์ธ์ฆ๋์ง ์์ ์ฌ์ฉ์์
๋๋ค.");
const result = await PerfumeServices.togglePerfumeLikeService(id, user.id);
if (!result.success) {
diff --git a/src/lib/hono/routes/v1/point.handler.ts b/src/server/hono/routes/v1/point.handler.ts
similarity index 86%
rename from src/lib/hono/routes/v1/point.handler.ts
rename to src/server/hono/routes/v1/point.handler.ts
index 67015086..393284ad 100644
--- a/src/lib/hono/routes/v1/point.handler.ts
+++ b/src/server/hono/routes/v1/point.handler.ts
@@ -1,15 +1,15 @@
import { createRoute } from "@hono/zod-openapi";
-import * as PointSchemas from "@/lib/hono/schemas/point.schema";
-import * as PointServices from "@/lib/hono/services/point.service";
+import * as PointSchemas from "@/server/hono/schemas/point.schema";
+import * as PointServices from "@/server/hono/services/point";
import {
apiBadRequest,
apiInternalError,
apiNotFound,
apiSuccess,
-} from "@/lib/hono/utils/api.utils";
-import { createStandardApiResponses } from "@/lib/hono/utils/openapi.schema";
-import { createDomainRouters } from "@/lib/hono/utils/router";
-import { getAuthenticatedUser } from "@/lib/hono/utils/service.utils";
+} from "@/server/hono/utils/api.utils";
+import { createStandardApiResponses } from "@/server/hono/utils/openapi.schema";
+import { createDomainRouters } from "@/server/hono/utils/router";
+
const routers = createDomainRouters();
@@ -30,7 +30,8 @@ const getUserPointsRoute = createRoute({
});
routers.authenticated.openapi(getUserPointsRoute, async (c) => {
- const user = getAuthenticatedUser(c);
+ const user = c.get("user");
+ if (!user?.id) throw new Error("์ธ์ฆ๋์ง ์์ ์ฌ์ฉ์์
๋๋ค.");
const result = await PointServices.getUserPointsService(user.id);
if (!result.success) {
@@ -64,7 +65,8 @@ const getPointHistoryRoute = createRoute({
});
routers.authenticated.openapi(getPointHistoryRoute, async (c) => {
- const user = getAuthenticatedUser(c);
+ const user = c.get("user");
+ if (!user?.id) throw new Error("์ธ์ฆ๋์ง ์์ ์ฌ์ฉ์์
๋๋ค.");
const query = c.req.valid("query");
const result = await PointServices.getPointHistoryService(user.id, {
@@ -100,7 +102,8 @@ const getPointStatisticsRoute = createRoute({
});
routers.authenticated.openapi(getPointStatisticsRoute, async (c) => {
- const user = getAuthenticatedUser(c);
+ const user = c.get("user");
+ if (!user?.id) throw new Error("์ธ์ฆ๋์ง ์์ ์ฌ์ฉ์์
๋๋ค.");
const result = await PointServices.getPointStatisticsService(user.id);
if (!result.success) {
@@ -139,7 +142,8 @@ const processLoginRoute = createRoute({
});
routers.authenticated.openapi(processLoginRoute, async (c) => {
- const user = getAuthenticatedUser(c);
+ const user = c.get("user");
+ if (!user?.id) throw new Error("์ธ์ฆ๋์ง ์์ ์ฌ์ฉ์์
๋๋ค.");
const body = c.req.valid("json");
// ํด๋ผ์ด์ธํธ ํ์์คํฌํ ๊ฒ์ฆ (5๋ถ ์ด๋ด์ ์์ฒญ๋ง ํ์ฉ)
diff --git a/src/lib/hono/routes/v1/review.handler.ts b/src/server/hono/routes/v1/review.handler.ts
similarity index 90%
rename from src/lib/hono/routes/v1/review.handler.ts
rename to src/server/hono/routes/v1/review.handler.ts
index aac748da..b6bdfa96 100644
--- a/src/lib/hono/routes/v1/review.handler.ts
+++ b/src/server/hono/routes/v1/review.handler.ts
@@ -1,9 +1,9 @@
import { createRoute, z } from "@hono/zod-openapi";
-import * as CommonSchemas from "@/lib/hono/schemas/common.schema";
-import * as ReviewSchemas from "@/lib/hono/schemas/review.schema";
+import * as CommonSchemas from "@/server/hono/schemas/common.schema";
+import * as ReviewSchemas from "@/server/hono/schemas/review.schema";
-import * as ReviewServices from "@/lib/hono/services/review.service";
+import * as ReviewServices from "@/server/hono/services/review";
import {
apiBadRequest,
@@ -12,10 +12,10 @@ import {
apiInternalError,
apiNotFound,
apiSuccess,
-} from "@/lib/hono/utils/api.utils";
-import { createStandardApiResponses } from "@/lib/hono/utils/openapi.schema";
-import { createDomainRouters } from "@/lib/hono/utils/router";
-import { getAuthenticatedUser } from "@/lib/hono/utils/service.utils";
+} from "@/server/hono/utils/api.utils";
+import { createStandardApiResponses } from "@/server/hono/utils/openapi.schema";
+import { createDomainRouters } from "@/server/hono/utils/router";
+
const routers = createDomainRouters();
@@ -144,7 +144,8 @@ const createReviewRoute = createRoute({
routers.authenticated.openapi(createReviewRoute, async (c) => {
const { perfumeId } = c.req.param();
const validatedData = c.req.valid("json");
- const user = getAuthenticatedUser(c);
+ const user = c.get("user");
+ if (!user?.id) throw new Error("์ธ์ฆ๋์ง ์์ ์ฌ์ฉ์์
๋๋ค.");
const payload = { ...validatedData, perfumeId, authorId: user.id };
@@ -188,7 +189,8 @@ const toggleLikeRoute = createRoute({
routers.authenticated.openapi(toggleLikeRoute, async (c) => {
const { reviewId } = c.req.param();
- const user = getAuthenticatedUser(c);
+ const user = c.get("user");
+ if (!user?.id) throw new Error("์ธ์ฆ๋์ง ์์ ์ฌ์ฉ์์
๋๋ค.");
const result = await ReviewServices.toggleLikeService(reviewId, user.id);
diff --git a/src/lib/hono/routes/v1/search.handler.ts b/src/server/hono/routes/v1/search.handler.ts
similarity index 83%
rename from src/lib/hono/routes/v1/search.handler.ts
rename to src/server/hono/routes/v1/search.handler.ts
index 3647b9eb..dd34cb90 100644
--- a/src/lib/hono/routes/v1/search.handler.ts
+++ b/src/server/hono/routes/v1/search.handler.ts
@@ -1,9 +1,9 @@
import { createRoute } from "@hono/zod-openapi";
-import * as SearchSchemas from "@/lib/hono/schemas/search.schema";
-import { searchPerfumesService } from "@/lib/hono/services/search.service";
-import { apiInternalError, apiSuccess } from "@/lib/hono/utils/api.utils";
-import { createStandardApiResponses } from "@/lib/hono/utils/openapi.schema";
-import { createDomainRouters } from "@/lib/hono/utils/router";
+import * as SearchSchemas from "@/server/hono/schemas/search.schema";
+import { searchPerfumesService } from "@/server/hono/services/search.service";
+import { apiInternalError, apiSuccess } from "@/server/hono/utils/api.utils";
+import { createStandardApiResponses } from "@/server/hono/utils/openapi.schema";
+import { createDomainRouters } from "@/server/hono/utils/router";
const routers = createDomainRouters();
diff --git a/src/lib/hono/routes/v1/user.handler.ts b/src/server/hono/routes/v1/user.handler.ts
similarity index 86%
rename from src/lib/hono/routes/v1/user.handler.ts
rename to src/server/hono/routes/v1/user.handler.ts
index f61ba03d..388e5dca 100644
--- a/src/lib/hono/routes/v1/user.handler.ts
+++ b/src/server/hono/routes/v1/user.handler.ts
@@ -1,15 +1,15 @@
import { createRoute, z } from "@hono/zod-openapi";
-import { ApiMyCollectionResponseSchema } from "@/lib/hono/schemas/me.schema";
-import { ApiPerfumeSimpleResponseSchema } from "@/lib/hono/schemas/perfume.schema";
-import * as UserSchemas from "@/lib/hono/schemas/user.schema";
-import * as UserServices from "@/lib/hono/services/user.service";
+import { ApiMyCollectionResponseSchema } from "@/server/hono/schemas/me.schema";
+import { ApiPerfumeSimpleResponseSchema } from "@/server/hono/schemas/perfume.schema";
+import * as UserSchemas from "@/server/hono/schemas/user.schema";
+import * as UserServices from "@/server/hono/services/user.service";
import {
apiInternalError,
apiNotFound,
apiSuccess,
-} from "@/lib/hono/utils/api.utils";
-import { createStandardApiResponses } from "@/lib/hono/utils/openapi.schema";
-import { createDomainRouters } from "@/lib/hono/utils/router";
+} from "@/server/hono/utils/api.utils";
+import { createStandardApiResponses } from "@/server/hono/utils/openapi.schema";
+import { createDomainRouters } from "@/server/hono/utils/router";
const routers = createDomainRouters();
diff --git a/src/lib/hono/schemas/brand.schema.ts b/src/server/hono/schemas/brand.schema.ts
similarity index 60%
rename from src/lib/hono/schemas/brand.schema.ts
rename to src/server/hono/schemas/brand.schema.ts
index aa9b4fa8..9f28b9a6 100644
--- a/src/lib/hono/schemas/brand.schema.ts
+++ b/src/server/hono/schemas/brand.schema.ts
@@ -1,5 +1,5 @@
import { z } from "@hono/zod-openapi";
-import { BrandSchema } from "@zod/modelSchema";
+import { BrandModelSchema as BrandSchema } from "@zod/schemas/variants/pure";
import { MapLocationSchema } from "./common.schema";
/**
@@ -62,6 +62,42 @@ export const ApiBrandForEmbeddingResponseSchema = BrandSchema.pick({
brandUrl: true,
});
+export const StoreSchema = z
+ .object({
+ name: z.string(),
+ address: z.string(),
+ roadAddress: z.string().optional(),
+ telephone: z.string().optional(),
+ x: z.string(),
+ y: z.string(),
+ category: z.string().optional(),
+ categoryGroupCode: z.string().optional(),
+ link: z.string().optional(),
+ distance: z.number().optional(),
+ })
+ .openapi("Store");
+
+export const BrandStoreNameParamSchema = z.object({
+ name: z.string().openapi({
+ param: { name: "name", in: "path" },
+ example: "๋ฅํฐํฌ",
+ description: "๊ฒ์ํ ๋ธ๋๋ ์ด๋ฆ",
+ }),
+});
+
+export const BrandStoreQuerySchema = z.object({
+ x: z.string().optional().openapi({ description: "์ฌ์ฉ์ ๊ฒฝ๋" }),
+ y: z.string().optional().openapi({ description: "์ฌ์ฉ์ ์๋" }),
+});
+
+export const BrandStoresResponseSchema = z
+ .object({
+ success: z.literal(true),
+ stores: z.array(StoreSchema),
+ total: z.number(),
+ })
+ .openapi("BrandStoresResponse");
+
export type ApiBrandSimpleResponse = z.infer<
typeof ApiBrandSimpleResponseSchema
>;
@@ -69,3 +105,5 @@ export type ApiBrandSimpleResponse = z.infer<
export type ApiBrandDetailResponse = z.infer<
typeof ApiBrandDetailResponseSchema
>;
+
+export type Store = z.infer;
diff --git a/src/lib/hono/schemas/comment.schema.ts b/src/server/hono/schemas/comment.schema.ts
similarity index 95%
rename from src/lib/hono/schemas/comment.schema.ts
rename to src/server/hono/schemas/comment.schema.ts
index 05ba5227..17ebf0b5 100644
--- a/src/lib/hono/schemas/comment.schema.ts
+++ b/src/server/hono/schemas/comment.schema.ts
@@ -1,5 +1,7 @@
import { z } from "@hono/zod-openapi";
-import { CommentSchema, UserSchema } from "@zod/modelSchema";
+import { CommentModelSchema, UserModelSchema as UserSchema } from "@zod/schemas/variants/pure";
+
+const CommentSchema = CommentModelSchema.omit({ replies: true, parent: true, post: true, author: true });
import {
CursorPaginationSchema,
PaginatedResponse,
diff --git a/src/lib/hono/schemas/common.schema.ts b/src/server/hono/schemas/common.schema.ts
similarity index 100%
rename from src/lib/hono/schemas/common.schema.ts
rename to src/server/hono/schemas/common.schema.ts
diff --git a/src/lib/hono/schemas/community.schema.ts b/src/server/hono/schemas/community.schema.ts
similarity index 94%
rename from src/lib/hono/schemas/community.schema.ts
rename to src/server/hono/schemas/community.schema.ts
index 50a923a3..31a0a4b3 100644
--- a/src/lib/hono/schemas/community.schema.ts
+++ b/src/server/hono/schemas/community.schema.ts
@@ -1,5 +1,7 @@
import { z } from "@hono/zod-openapi";
-import { PostSchema, UserSchema } from "@zod/modelSchema";
+import { PostModelSchema, UserModelSchema as UserSchema } from "@zod/schemas/variants/pure";
+
+const PostSchema = PostModelSchema.omit({ comments: true, bookmarks: true, likes: true, perfumeMappings: true, recentViews: true, author: true });
import {
CursorPaginationSchema,
PaginatedResponse,
@@ -167,3 +169,5 @@ export type GetPostsQuery = z.infer;
export type CreatePostInput = z.infer;
export type UpdatePostInput = z.infer;
export type PerfumeForPost = z.infer;
+
+export { PostCategory } from "@prisma/client";
diff --git a/src/lib/hono/schemas/draft.schema.ts b/src/server/hono/schemas/draft.schema.ts
similarity index 83%
rename from src/lib/hono/schemas/draft.schema.ts
rename to src/server/hono/schemas/draft.schema.ts
index ed538196..62f01a08 100644
--- a/src/lib/hono/schemas/draft.schema.ts
+++ b/src/server/hono/schemas/draft.schema.ts
@@ -66,6 +66,14 @@ export const DraftSuccessResponseSchema = z.object({
draft: ApiDraftResponseSchema.optional(),
});
+/**
+ * ์์ ์ ์ฅ ๋ชฉ๋ก/๋จ์ผ ์กฐํ ์ฟผ๋ฆฌ ์คํค๋ง
+ * @description type์ ์ง์ ํ๋ฉด ํด๋น ํ์
์ ๋จ์ผ ์์ ์ ์ฅ์, ์๋ตํ๋ฉด ์ ์ฒด ๋ชฉ๋ก์ ๋ฐํ
+ */
+export const ListDraftQuerySchema = z.object({
+ type: DraftTypeSchema.optional().openapi({ description: "์กฐํํ ์์ ์ ์ฅ ํ์
(์ง์ ์ ๋จ์ผ ๋ฐํ, ์๋ต ์ ์ ์ฒด ๋ชฉ๋ก ๋ฐํ)" }),
+});
+
/**
* ์์ ์ ์ฅ ID ํ๋ผ๋ฏธํฐ ์คํค๋ง
* @description URL ๊ฒฝ๋ก์์ ์ฌ์ฉ๋๋ ์์ ์ ์ฅ ID ๊ฒ์ฆ
@@ -74,8 +82,11 @@ export const DraftIdParamSchema = z.object({
id: z.string().uuid(),
});
+export { DraftType } from "@prisma/client";
+
export type CreateDraftBody = z.infer;
export type CreateDraftPayload = z.infer;
export type ApiDraftResponse = z.infer;
export type ApiDraftListResponse = z.infer;
export type DraftSuccessResponse = z.infer;
+export type ListDraftQuery = z.infer;
diff --git a/src/lib/hono/schemas/filter.schema.ts b/src/server/hono/schemas/filter.schema.ts
similarity index 96%
rename from src/lib/hono/schemas/filter.schema.ts
rename to src/server/hono/schemas/filter.schema.ts
index 78cb01fe..d6442bce 100644
--- a/src/lib/hono/schemas/filter.schema.ts
+++ b/src/server/hono/schemas/filter.schema.ts
@@ -1,5 +1,5 @@
import { z } from "@hono/zod-openapi";
-import { SearchPostBodySchema } from "@/lib/hono/schemas/search.schema";
+import { SearchPostBodySchema } from "@/server/hono/schemas/search.schema";
/**
* ๋ฐ์ดํฐ๋ฒ ์ด์ค ํํฐ ์ต์
์คํค๋ง
diff --git a/src/lib/hono/schemas/me.schema.ts b/src/server/hono/schemas/me.schema.ts
similarity index 95%
rename from src/lib/hono/schemas/me.schema.ts
rename to src/server/hono/schemas/me.schema.ts
index 6d02962c..5768163c 100644
--- a/src/lib/hono/schemas/me.schema.ts
+++ b/src/server/hono/schemas/me.schema.ts
@@ -1,10 +1,10 @@
import { z } from "@hono/zod-openapi";
import {
- UserCollectionSchema,
- CollectionImageSchema,
- UserSchema,
-} from "@zod/modelSchema";
-import { ImageFormatSchema } from "@zod/inputTypeSchemas";
+ UserCollectionModelSchema as UserCollectionSchema,
+ CollectionImageModelSchema as CollectionImageSchema,
+ UserModelSchema as UserSchema,
+} from "@zod/schemas/variants/pure";
+import { ImageFormatSchema } from "@zod/schemas";
import { ApiPostResponseSchema } from "./community.schema";
import { ApiPerfumeSimpleResponseSchema } from "./perfume.schema";
import { ApiReviewResponseSchema } from "./review.schema";
@@ -104,7 +104,7 @@ export const ApiMyProfileResponseSchema = UserSchema.pick({
export const ApiUpdateMyProfileRequestSchema = z
.object({
- nickname: UserSchema.shape.nickname.optional(),
+ nickname: z.string().min(2).max(20).optional(),
age: UserSchema.shape.age.optional(),
gender: UserSchema.shape.gender.optional(),
imageUrl: z.string().url().optional(),
diff --git a/src/lib/hono/schemas/perfume.schema.ts b/src/server/hono/schemas/perfume.schema.ts
similarity index 85%
rename from src/lib/hono/schemas/perfume.schema.ts
rename to src/server/hono/schemas/perfume.schema.ts
index 37c08c95..e5e4a099 100644
--- a/src/lib/hono/schemas/perfume.schema.ts
+++ b/src/server/hono/schemas/perfume.schema.ts
@@ -1,12 +1,12 @@
import { z } from "@hono/zod-openapi";
import {
- PerfumeSchema,
- PerfumeImageSchema,
- PerfumeAccordSchema,
- PerfumeNoteSchema,
- ReviewSchema,
- UserSchema,
-} from "@zod/modelSchema";
+ PerfumeModelSchema as PerfumeSchema,
+ PerfumeImageModelSchema as PerfumeImageSchema,
+ PerfumeAccordModelSchema as PerfumeAccordSchema,
+ PerfumeNoteModelSchema as PerfumeNoteSchema,
+ ReviewModelSchema as ReviewSchema,
+ UserModelSchema as UserSchema,
+} from "@zod/schemas/variants/pure";
import { ApiBrandForEmbeddingResponseSchema } from "./brand.schema";
/**
@@ -47,7 +47,7 @@ export const ApiPerfumeSimpleResponseSchema = PerfumeBaseResponseSchema.pick({
nameKo: true,
brand: true,
perfumeImage: true,
-});
+}).openapi("ApiPerfumeSimpleResponse");
/**
* ํฅ์ ์์ธ ์๋ต ์คํค๋ง
@@ -95,3 +95,5 @@ export type ApiPerfumeNoteResponse = z.infer<
export type ApiPerfumeAccordResponse = z.infer<
typeof ApiPerfumeAccordResponseSchema
>;
+
+export type { PerfumeNote, PerfumeAccord } from "@prisma/client";
diff --git a/src/lib/hono/schemas/point.schema.ts b/src/server/hono/schemas/point.schema.ts
similarity index 98%
rename from src/lib/hono/schemas/point.schema.ts
rename to src/server/hono/schemas/point.schema.ts
index 61e638ec..8055c85c 100644
--- a/src/lib/hono/schemas/point.schema.ts
+++ b/src/server/hono/schemas/point.schema.ts
@@ -1,6 +1,6 @@
import { z } from "@hono/zod-openapi";
import { PointActivityType } from "@prisma/client";
-import { PointHistorySchema } from "@zod/modelSchema";
+import { PointHistoryModelSchema as PointHistorySchema } from "@zod/schemas/variants/pure";
/**
* ํฌ์ธํธ ํ๋ ํ์
์คํค๋ง
diff --git a/src/lib/hono/schemas/review.schema.ts b/src/server/hono/schemas/review.schema.ts
similarity index 81%
rename from src/lib/hono/schemas/review.schema.ts
rename to src/server/hono/schemas/review.schema.ts
index 0111c410..91a2fe71 100644
--- a/src/lib/hono/schemas/review.schema.ts
+++ b/src/server/hono/schemas/review.schema.ts
@@ -1,10 +1,15 @@
import { z } from "@hono/zod-openapi";
import {
- ReviewSchema,
- ReviewAttributeSelectionSchema,
- AttributeOptionSchema,
- ReviewAttributeSchema,
-} from "@zod/modelSchema";
+ ReviewModelSchema,
+ ReviewAttributeSelectionModelSchema,
+ AttributeOptionModelSchema,
+ ReviewAttributeModelSchema,
+} from "@zod/schemas/variants/pure";
+
+const ReviewSchema = ReviewModelSchema.omit({ likes: true, attributeSelections: true, author: true, perfume: true });
+const ReviewAttributeSelectionSchema = ReviewAttributeSelectionModelSchema.omit({ option: true, review: true });
+const AttributeOptionSchema = AttributeOptionModelSchema.omit({ attribute: true, selections: true });
+const ReviewAttributeSchema = ReviewAttributeModelSchema.omit({ options: true });
import { PaginatedResponseSchema } from "./common.schema";
import { ApiPerfumeSimpleResponseSchema } from "./perfume.schema";
import { ApiReviewAuthorResponseSchema } from "./user.schema";
@@ -86,3 +91,5 @@ export type ApiReviewResponse = z.infer;
export type CreateReviewInput = z.infer;
export type UpdateReviewInput = z.infer;
+
+export { PerfumeUsageStatus } from "@prisma/client";
diff --git a/src/lib/hono/schemas/search.schema.ts b/src/server/hono/schemas/search.schema.ts
similarity index 78%
rename from src/lib/hono/schemas/search.schema.ts
rename to src/server/hono/schemas/search.schema.ts
index a6ffca9b..d9f6e622 100644
--- a/src/lib/hono/schemas/search.schema.ts
+++ b/src/server/hono/schemas/search.schema.ts
@@ -13,8 +13,23 @@ import {
* ๊ฒ์ GET ์์ฒญ ์ฟผ๋ฆฌ ํ๋ผ๋ฏธํฐ ์คํค๋ง
* @description ๊ธฐ๋ณธ ๊ฒ์ ์์ฒญ ์ ์ฌ์ฉ๋๋ ์ฟผ๋ฆฌ ํ๋ผ๋ฏธํฐ (๊ฒ์์ด, ํ์ด์ง๋ค์ด์
)
*/
+export const GenderSortSchema = z
+ .enum(["MASCULINE", "UNISEX", "FEMININE"])
+ .default("UNISEX");
+
+export type GenderSort = z.infer;
+
export const SearchGetQuerySchema = CursorPaginationSchema.extend({
searchText: z.string().optional().default(""),
+ // ์ปค์ ํ์: "{gender_count}:{priority}:{perfume_id}"
+ cursor: z
+ .string()
+ .regex(
+ /^\d+:\d+:[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/,
+ "์ ํจํ์ง ์์ ์ปค์ ํ์์
๋๋ค."
+ )
+ .optional(),
+ genderSort: GenderSortSchema.optional(),
});
/**
@@ -33,7 +48,7 @@ export const SearchPostBodySchema = SearchGetQuerySchema.extend({
*/
export const PaginatedSearchResponseSchema = PaginatedResponseSchema(
ApiPerfumeSimpleResponseSchema
-);
+).openapi("PaginatedSearchResponse");
/**
* Supabase ํฅ์ ์คํค๋ง
@@ -49,6 +64,7 @@ export const SupabasePerfumeSchema = z.object({
brand_url: z.string().nullable(),
image_url: z.string(),
priority: z.number().int(),
+ gender_vote_count: z.number().int(),
});
export type SearchGetQuery = z.infer;
diff --git a/src/lib/hono/schemas/user.schema.ts b/src/server/hono/schemas/user.schema.ts
similarity index 93%
rename from src/lib/hono/schemas/user.schema.ts
rename to src/server/hono/schemas/user.schema.ts
index f57e1074..0f611e5a 100644
--- a/src/lib/hono/schemas/user.schema.ts
+++ b/src/server/hono/schemas/user.schema.ts
@@ -1,5 +1,5 @@
import { z } from "@hono/zod-openapi";
-import { UserSchema } from "@zod/modelSchema";
+import { UserModelSchema as UserSchema } from "@zod/schemas/variants/pure";
/**
* ์ฌ์ฉ์ ID ๊ฒฝ๋ก ํ๋ผ๋ฏธํฐ ์คํค๋ง
diff --git a/src/server/hono/services/__tests__/auth.service.test.ts b/src/server/hono/services/__tests__/auth.service.test.ts
new file mode 100644
index 00000000..c4c0eb39
--- /dev/null
+++ b/src/server/hono/services/__tests__/auth.service.test.ts
@@ -0,0 +1,535 @@
+import { describe, it, expect, beforeEach, vi } from "vitest";
+import { prisma } from "@/server/prisma";
+import {
+ syncOAuthUserService,
+ confirmOAuthLinkService,
+ getSessionUserService,
+} from "../auth.service";
+
+vi.mock("@/server/prisma", () => ({
+ prisma: {
+ user: {
+ findUnique: vi.fn(),
+ update: vi.fn(),
+ create: vi.fn(),
+ },
+ userOAuthAccount: {
+ findUnique: vi.fn(),
+ findFirst: vi.fn(),
+ create: vi.fn(),
+ count: vi.fn(),
+ delete: vi.fn(),
+ deleteMany: vi.fn(),
+ },
+ pendingOAuthLink: {
+ findUnique: vi.fn(),
+ create: vi.fn(),
+ delete: vi.fn(),
+ },
+ },
+}));
+
+vi.mock("uuid", () => ({
+ v4: vi.fn(() => "test-auth-id-1234-5678"),
+}));
+
+vi.mock("@/server/hono/utils/nickname.utils", () => ({
+ generateNicknameCandidate: vi.fn(() => "ํ๋ก๋ดํ ๋ผ4821"),
+}));
+
+const BASE_INPUT = {
+ provider: "google",
+ providerAccountId: "google-uid-001",
+ email: "test@example.com",
+};
+
+const ACTIVE_USER = {
+ id: "user-001",
+ email: "test@example.com",
+ nickname: "user_1234",
+ isActive: true,
+ deletedAt: null,
+};
+
+const DELETED_USER_RECENT = {
+ id: "user-002",
+ email: "test@example.com",
+ nickname: "user_5678_deleted_1234567890",
+ isActive: false,
+ deletedAt: new Date(Date.now() - 3 * 24 * 60 * 60 * 1000), // 3์ผ ์
+};
+
+const DELETED_USER_OLD = {
+ id: "user-003",
+ email: "test@example.com",
+ nickname: "user_9012_deleted_9876543210",
+ isActive: false,
+ deletedAt: new Date(Date.now() - 10 * 24 * 60 * 60 * 1000), // 10์ผ ์
+};
+
+describe("syncOAuthUserService", () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ });
+
+ describe("OAuth ๊ณ์ ์ผ๋ก ๊ธฐ์กด ์ ์ ์กฐํ (์ฌ๋ก๊ทธ์ธ)", () => {
+ it("providerAccountId๋ก ์ฐพ์ ํ์ฑ ์ ์ ๋ฅผ ๋ฐํํด์ผ ํ๋ค", async () => {
+ vi.mocked(prisma.userOAuthAccount.findFirst).mockResolvedValue({
+ id: "oauth-001",
+ userId: ACTIVE_USER.id,
+ provider: "google",
+ providerAccountId: "google-uid-001",
+ createdAt: new Date(),
+ user: ACTIVE_USER,
+ } as never);
+
+ const result = await syncOAuthUserService(BASE_INPUT);
+
+ expect(result.success).toBe(true);
+ if (result.success && result.data.type === "success") {
+ expect(result.data.id).toBe("user-001");
+ expect(result.data.isNewUser).toBe(false);
+ }
+ expect(prisma.user.findUnique).not.toHaveBeenCalled();
+ expect(prisma.user.create).not.toHaveBeenCalled();
+ });
+
+ it("providerAccountId๋ก ์ฐพ์ ์ ์ ๊ฐ ํํด ํ 7์ผ ๋ฏธ๋ง์ด๋ฉด ์ฌํ์ฑํํด์ผ ํ๋ค", async () => {
+ const reactivated = {
+ ...DELETED_USER_RECENT,
+ isActive: true,
+ deletedAt: null,
+ nickname: "user_5678",
+ };
+ vi.mocked(prisma.userOAuthAccount.findFirst).mockResolvedValue({
+ id: "oauth-002",
+ userId: DELETED_USER_RECENT.id,
+ provider: "google",
+ providerAccountId: "google-uid-001",
+ createdAt: new Date(),
+ user: DELETED_USER_RECENT,
+ } as never);
+ vi.mocked(prisma.user.update).mockResolvedValue(reactivated as never);
+
+ const result = await syncOAuthUserService(BASE_INPUT);
+
+ expect(result.success).toBe(true);
+ if (result.success && result.data.type === "success") {
+ expect(result.data.id).toBe("user-002");
+ expect(result.data.isNewUser).toBe(false);
+ }
+ expect(prisma.user.update).toHaveBeenCalledWith(
+ expect.objectContaining({
+ where: { id: "user-002" },
+ data: expect.objectContaining({
+ isActive: true,
+ deletedAt: null,
+ nickname: "user_5678",
+ }),
+ }),
+ );
+ });
+
+ it("providerAccountId๋ก ์ฐพ์ ์ ์ ๊ฐ ํํด ํ 7์ผ ์ด์์ด๋ฉด OAuth ๊ณ์ ์ ํด์ ํ๊ณ ์ ๊ท ์์ฑํด์ผ ํ๋ค", async () => {
+ const newUser = {
+ id: "user-new",
+ email: "test@example.com",
+ nickname: "user_4321",
+ isActive: true,
+ deletedAt: null,
+ };
+ vi.mocked(prisma.userOAuthAccount.findFirst).mockResolvedValueOnce({
+ id: "oauth-003",
+ userId: DELETED_USER_OLD.id,
+ provider: "google",
+ providerAccountId: "google-uid-001",
+ createdAt: new Date(),
+ user: DELETED_USER_OLD,
+ } as never);
+ vi.mocked(prisma.user.update).mockResolvedValue({
+ ...DELETED_USER_OLD,
+ email: null,
+ } as never);
+ vi.mocked(prisma.user.findUnique).mockResolvedValueOnce(null); // email ์กฐํ โ ์์
+ vi.mocked(prisma.user.findUnique).mockResolvedValueOnce(null); // nickname ์ค๋ณต ๊ฒ์ฌ
+ vi.mocked(prisma.user.create).mockResolvedValue(newUser as never);
+ vi.mocked(prisma.userOAuthAccount.create).mockResolvedValue({} as never);
+
+ const result = await syncOAuthUserService(BASE_INPUT);
+
+ expect(result.success).toBe(true);
+ if (result.success && result.data.type === "success") {
+ expect(result.data.isNewUser).toBe(true);
+ }
+ expect(prisma.userOAuthAccount.deleteMany).toHaveBeenCalledWith({
+ where: { provider: "google", providerAccountId: "google-uid-001" },
+ });
+ expect(prisma.user.update).toHaveBeenCalledWith(
+ expect.objectContaining({ data: { email: null } }),
+ );
+ expect(prisma.user.create).toHaveBeenCalled();
+ expect(prisma.userOAuthAccount.create).toHaveBeenCalled();
+ });
+ });
+
+ describe("email๋ก ๊ธฐ์กด ์ ์ ์กฐํ (์ด๋ฉ์ผ ์ค๋ณต)", () => {
+ it("OAuth ๊ณ์ ์๊ณ email๋ก ํ์ฑ ์ ์ ๋ฅผ ์ฐพ์ผ๋ฉด pendingOAuthLink๋ฅผ ์์ฑํ๊ณ email_conflict๋ฅผ ๋ฐํํด์ผ ํ๋ค", async () => {
+ vi.mocked(prisma.userOAuthAccount.findFirst).mockResolvedValue(null);
+ vi.mocked(prisma.user.findUnique).mockResolvedValue(ACTIVE_USER as never);
+ vi.mocked(prisma.pendingOAuthLink.create).mockResolvedValue({
+ id: "pending-001",
+ token: "token-abc-123",
+ } as never);
+
+ const result = await syncOAuthUserService(BASE_INPUT);
+
+ expect(result.success).toBe(true);
+ if (result.success) {
+ expect(result.data.type).toBe("email_conflict");
+ if (result.data.type === "email_conflict") {
+ expect(result.data.token).toBe("token-abc-123");
+ }
+ }
+ expect(prisma.pendingOAuthLink.create).toHaveBeenCalledWith(
+ expect.objectContaining({
+ data: expect.objectContaining({
+ provider: "google",
+ providerAccountId: "google-uid-001",
+ targetEmail: "test@example.com",
+ }),
+ }),
+ );
+ expect(prisma.userOAuthAccount.create).not.toHaveBeenCalled();
+ });
+
+ it("OAuth ๊ณ์ ์๊ณ email๋ก ์ฐพ์ ์ ์ ๊ฐ ํํด ํ 7์ผ ๋ฏธ๋ง์ด๋ฉด ์ฌํ์ฑํ + OAuth ๊ณ์ ์ฐ๊ฒฐํด์ผ ํ๋ค", async () => {
+ const reactivated = {
+ ...DELETED_USER_RECENT,
+ isActive: true,
+ deletedAt: null,
+ nickname: "user_5678",
+ };
+ vi.mocked(prisma.userOAuthAccount.findFirst).mockResolvedValue(null);
+ vi.mocked(prisma.user.findUnique).mockResolvedValue(
+ DELETED_USER_RECENT as never,
+ );
+ vi.mocked(prisma.user.update).mockResolvedValue(reactivated as never);
+ vi.mocked(prisma.userOAuthAccount.create).mockResolvedValue({} as never);
+
+ const result = await syncOAuthUserService(BASE_INPUT);
+
+ expect(result.success).toBe(true);
+ if (result.success && result.data.type === "success") {
+ expect(result.data.id).toBe("user-002");
+ expect(result.data.isNewUser).toBe(false);
+ }
+ expect(prisma.user.update).toHaveBeenCalledWith(
+ expect.objectContaining({
+ data: expect.objectContaining({ isActive: true, deletedAt: null }),
+ }),
+ );
+ expect(prisma.userOAuthAccount.create).toHaveBeenCalledWith({
+ data: {
+ userId: "user-002",
+ provider: "google",
+ providerAccountId: "google-uid-001",
+ },
+ });
+ expect(prisma.pendingOAuthLink.create).not.toHaveBeenCalled();
+ });
+
+ it("OAuth ๊ณ์ ์๊ณ email๋ก ์ฐพ์ ์ ์ ๊ฐ ํํด ํ 7์ผ ์ด์์ด๋ฉด email ํด์ ํ ์ ๊ท ์์ฑํด์ผ ํ๋ค", async () => {
+ const newUser = {
+ id: "user-new",
+ email: "test@example.com",
+ nickname: "user_4321",
+ isActive: true,
+ deletedAt: null,
+ };
+ vi.mocked(prisma.userOAuthAccount.findFirst).mockResolvedValue(null);
+ vi.mocked(prisma.user.findUnique)
+ .mockResolvedValueOnce(DELETED_USER_OLD as never) // email ์กฐํ
+ .mockResolvedValueOnce(null); // nickname ์ค๋ณต ๊ฒ์ฌ
+ vi.mocked(prisma.user.update).mockResolvedValue({
+ ...DELETED_USER_OLD,
+ email: null,
+ } as never);
+ vi.mocked(prisma.user.create).mockResolvedValue(newUser as never);
+ vi.mocked(prisma.userOAuthAccount.create).mockResolvedValue({} as never);
+
+ const result = await syncOAuthUserService(BASE_INPUT);
+
+ expect(result.success).toBe(true);
+ if (result.success && result.data.type === "success") {
+ expect(result.data.isNewUser).toBe(true);
+ }
+ expect(prisma.user.update).toHaveBeenCalledWith(
+ expect.objectContaining({ data: { email: null } }),
+ );
+ expect(prisma.user.create).toHaveBeenCalled();
+ expect(prisma.userOAuthAccount.create).toHaveBeenCalled();
+ });
+ });
+
+ describe("์ ๊ท ์ ์ ์์ฑ", () => {
+ it("OAuth ๊ณ์ ๋ ์๊ณ email๋ก๋ ์ฐพ์ ์ ์์ผ๋ฉด ์ ๊ท ์ ์ ์ OAuth ๊ณ์ ์ ์์ฑํด์ผ ํ๋ค", async () => {
+ const newUser = {
+ id: "user-new",
+ email: "test@example.com",
+ nickname: "user_4321",
+ isActive: true,
+ deletedAt: null,
+ };
+ vi.mocked(prisma.userOAuthAccount.findFirst).mockResolvedValue(null);
+ vi.mocked(prisma.user.findUnique)
+ .mockResolvedValueOnce(null) // email ๊ฒ์
+ .mockResolvedValueOnce(null); // nickname ์ค๋ณต ๊ฒ์ฌ
+ vi.mocked(prisma.user.create).mockResolvedValue(newUser as never);
+ vi.mocked(prisma.userOAuthAccount.create).mockResolvedValue({} as never);
+
+ const result = await syncOAuthUserService(BASE_INPUT);
+
+ expect(result.success).toBe(true);
+ if (result.success && result.data.type === "success") {
+ expect(result.data.isNewUser).toBe(true);
+ }
+ expect(prisma.user.create).toHaveBeenCalledWith(
+ expect.objectContaining({
+ data: expect.objectContaining({
+ nickname: "ํ๋ก๋ดํ ๋ผ4821",
+ email: "test@example.com",
+ name: "",
+ totalPoints: 100,
+ }),
+ }),
+ );
+ expect(prisma.userOAuthAccount.create).toHaveBeenCalledWith({
+ data: {
+ userId: "user-new",
+ provider: "google",
+ providerAccountId: "google-uid-001",
+ },
+ });
+ });
+
+ it("nickname ํ๋ณด๊ฐ ์ค๋ณต์ด๋ฉด authId ๊ธฐ๋ฐ ํด๋ฐฑ nickname์ผ๋ก ์์ฑํด์ผ ํ๋ค", async () => {
+ const newUser = {
+ id: "user-new",
+ email: "test@example.com",
+ nickname: "user_test-aut",
+ isActive: true,
+ deletedAt: null,
+ };
+ vi.mocked(prisma.userOAuthAccount.findFirst).mockResolvedValue(null);
+ vi.mocked(prisma.user.findUnique)
+ .mockResolvedValueOnce(null) // email ๊ฒ์
+ .mockResolvedValueOnce({ id: "existing" } as never); // nickname ์ค๋ณต
+ vi.mocked(prisma.user.create).mockResolvedValue(newUser as never);
+ vi.mocked(prisma.userOAuthAccount.create).mockResolvedValue({} as never);
+
+ const result = await syncOAuthUserService(BASE_INPUT);
+
+ expect(result.success).toBe(true);
+ // uuid mock: "test-auth-id-1234-5678" โ authId.slice(0, 8) = "test-aut"
+ expect(prisma.user.create).toHaveBeenCalledWith(
+ expect.objectContaining({
+ data: expect.objectContaining({ nickname: "user_test-aut" }),
+ }),
+ );
+ });
+ });
+
+ describe("์๋ฌ ์ฒ๋ฆฌ", () => {
+ it("DB ์๋ฌ ์ INTERNAL_ERROR๋ฅผ ๋ฐํํด์ผ ํ๋ค", async () => {
+ vi.mocked(prisma.userOAuthAccount.findFirst).mockRejectedValue(
+ new Error("DB connection failed"),
+ );
+
+ const result = await syncOAuthUserService(BASE_INPUT);
+
+ expect(result.success).toBe(false);
+ if (!result.success) {
+ expect(result.error).toBe("INTERNAL_ERROR");
+ }
+ });
+
+ it("์ฌํ์ฑํ ์ค user.update ์๋ฌ ์ INTERNAL_ERROR๋ฅผ ๋ฐํํด์ผ ํ๋ค", async () => {
+ vi.mocked(prisma.userOAuthAccount.findFirst).mockResolvedValue({
+ id: "oauth-002",
+ userId: DELETED_USER_RECENT.id,
+ provider: "google",
+ providerAccountId: "google-uid-001",
+ createdAt: new Date(),
+ user: DELETED_USER_RECENT,
+ } as never);
+ vi.mocked(prisma.user.update).mockRejectedValue(new Error("DB error"));
+
+ const result = await syncOAuthUserService(BASE_INPUT);
+
+ expect(result.success).toBe(false);
+ if (!result.success) {
+ expect(result.error).toBe("INTERNAL_ERROR");
+ }
+ });
+ });
+});
+
+describe("confirmOAuthLinkService", () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ });
+
+ const VALID_PENDING = {
+ id: "pending-001",
+ token: "valid-token-uuid",
+ provider: "naver",
+ providerAccountId: "naver-uid-001",
+ targetEmail: "test@example.com",
+ expiresAt: new Date(Date.now() + 5 * 60 * 1000), // 5๋ถ ํ
+ createdAt: new Date(),
+ };
+
+ it("์ ํจํ ํ ํฐ์ผ๋ก OAuthAccount๋ฅผ ์์ฑํ๊ณ pending ๋ ์ฝ๋๋ฅผ ์ญ์ ํด์ผ ํ๋ค", async () => {
+ vi.mocked(prisma.pendingOAuthLink.findUnique).mockResolvedValue(
+ VALID_PENDING as never,
+ );
+ vi.mocked(prisma.user.findUnique).mockResolvedValue(ACTIVE_USER as never);
+ vi.mocked(prisma.userOAuthAccount.findUnique).mockResolvedValue(null);
+ vi.mocked(prisma.userOAuthAccount.create).mockResolvedValue({} as never);
+ vi.mocked(prisma.pendingOAuthLink.delete).mockResolvedValue({} as never);
+
+ const result = await confirmOAuthLinkService("valid-token-uuid");
+
+ expect(result.success).toBe(true);
+ if (result.success) {
+ expect(result.data.userId).toBe("user-001");
+ }
+ expect(prisma.userOAuthAccount.create).toHaveBeenCalledWith({
+ data: {
+ userId: "user-001",
+ provider: "naver",
+ providerAccountId: "naver-uid-001",
+ },
+ });
+ expect(prisma.pendingOAuthLink.delete).toHaveBeenCalledWith({
+ where: { token: "valid-token-uuid" },
+ });
+ });
+
+ it("์กด์ฌํ์ง ์๋ ํ ํฐ์ด๋ฉด NOT_FOUND๋ฅผ ๋ฐํํด์ผ ํ๋ค", async () => {
+ vi.mocked(prisma.pendingOAuthLink.findUnique).mockResolvedValue(null);
+
+ const result = await confirmOAuthLinkService("invalid-token");
+
+ expect(result.success).toBe(false);
+ if (!result.success) {
+ expect(result.error).toBe("NOT_FOUND");
+ }
+ });
+
+ it("๋ง๋ฃ๋ ํ ํฐ์ด๋ฉด BAD_REQUEST๋ฅผ ๋ฐํํ๊ณ pending ๋ ์ฝ๋๋ฅผ ์ญ์ ํด์ผ ํ๋ค", async () => {
+ const expiredPending = {
+ ...VALID_PENDING,
+ expiresAt: new Date(Date.now() - 1000), // ์ด๋ฏธ ๋ง๋ฃ
+ };
+ vi.mocked(prisma.pendingOAuthLink.findUnique).mockResolvedValue(
+ expiredPending as never,
+ );
+ vi.mocked(prisma.pendingOAuthLink.delete).mockResolvedValue({} as never);
+
+ const result = await confirmOAuthLinkService("expired-token");
+
+ expect(result.success).toBe(false);
+ if (!result.success) {
+ expect(result.error).toBe("BAD_REQUEST");
+ }
+ expect(prisma.pendingOAuthLink.delete).toHaveBeenCalled();
+ });
+
+ it("OAuthAccount๊ฐ ์ด๋ฏธ ์กด์ฌํ๋ฉด create๋ฅผ ๊ฑด๋๋ฐ๊ณ ์ฑ๊ณต์ ๋ฐํํด์ผ ํ๋ค", async () => {
+ vi.mocked(prisma.pendingOAuthLink.findUnique).mockResolvedValue(
+ VALID_PENDING as never,
+ );
+ vi.mocked(prisma.user.findUnique).mockResolvedValue(ACTIVE_USER as never);
+ vi.mocked(prisma.userOAuthAccount.findUnique).mockResolvedValue({
+ id: "existing-oauth",
+ } as never);
+ vi.mocked(prisma.pendingOAuthLink.delete).mockResolvedValue({} as never);
+
+ const result = await confirmOAuthLinkService("valid-token-uuid");
+
+ expect(result.success).toBe(true);
+ expect(prisma.userOAuthAccount.create).not.toHaveBeenCalled();
+ expect(prisma.pendingOAuthLink.delete).toHaveBeenCalled();
+ });
+});
+
+describe("getSessionUserService", () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ });
+
+ const ACTIVE_SESSION_USER = {
+ id: "user-001",
+ isActive: true,
+ nickname: "user_1234",
+ imageUrl: null,
+ };
+
+ it("ํ์ฑ ์ ์ ์ ์ต์ ์ ๋ณด๋ฅผ ๋ฐํํด์ผ ํ๋ค", async () => {
+ vi.mocked(prisma.user.findUnique).mockResolvedValue(
+ ACTIVE_SESSION_USER as never,
+ );
+
+ const result = await getSessionUserService("user-001");
+
+ expect(result.success).toBe(true);
+ if (result.success) {
+ expect(result.data.id).toBe("user-001");
+ expect(result.data.isActive).toBe(true);
+ expect(result.data.nickname).toBe("user_1234");
+ }
+ expect(prisma.user.findUnique).toHaveBeenCalledWith({
+ where: { id: "user-001" },
+ select: { id: true, isActive: true, nickname: true, imageUrl: true },
+ });
+ });
+
+ it("๋นํ์ฑ ์ ์ ๋ ๋ฐ์ดํฐ๋ฅผ ๋ฐํํด์ผ ํ๋ค (ํ์ฑ ์ฌ๋ถ ํ๋จ์ ํธ์ถ์ ์ฑ
์)", async () => {
+ vi.mocked(prisma.user.findUnique).mockResolvedValue({
+ ...ACTIVE_SESSION_USER,
+ isActive: false,
+ } as never);
+
+ const result = await getSessionUserService("user-001");
+
+ expect(result.success).toBe(true);
+ if (result.success) {
+ expect(result.data.isActive).toBe(false);
+ }
+ });
+
+ it("์กด์ฌํ์ง ์๋ ์ ์ ๋ NOT_FOUND๋ฅผ ๋ฐํํด์ผ ํ๋ค", async () => {
+ vi.mocked(prisma.user.findUnique).mockResolvedValue(null);
+
+ const result = await getSessionUserService("nonexistent");
+
+ expect(result.success).toBe(false);
+ if (!result.success) {
+ expect(result.error).toBe("NOT_FOUND");
+ }
+ });
+
+ it("DB ์๋ฌ ์ INTERNAL_ERROR๋ฅผ ๋ฐํํด์ผ ํ๋ค", async () => {
+ vi.mocked(prisma.user.findUnique).mockRejectedValue(
+ new Error("DB connection failed"),
+ );
+
+ const result = await getSessionUserService("user-001");
+
+ expect(result.success).toBe(false);
+ if (!result.success) {
+ expect(result.error).toBe("INTERNAL_ERROR");
+ }
+ });
+});
diff --git a/src/server/hono/services/__tests__/brand.service.test.ts b/src/server/hono/services/__tests__/brand.service.test.ts
new file mode 100644
index 00000000..a546d341
--- /dev/null
+++ b/src/server/hono/services/__tests__/brand.service.test.ts
@@ -0,0 +1,431 @@
+import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
+import { prisma } from "@/server/prisma";
+import {
+ getAllBrandsService,
+ getBrandByIdService,
+ getBrandByNameService,
+ getStoresByNameService,
+} from "../brand.service";
+
+/**
+ * Brand ์๋น์ค ํ
์คํธ
+ *
+ * ํ
์คํธ ์ ๋ต:
+ * - ๋ธ๋๋ CRUD ํต์ฌ ๋ก์ง ๊ฒ์ฆ
+ * - nameKo null ์ nameEn ๋์ฒด ๋ก์ง ํ์ธ
+ * - ์นด์นด์ค ๋ก์ปฌ ๊ฒ์ API ํธ์ถ ๋ฐ ์นดํ
๊ณ ๋ฆฌ ํํฐ๋ง ๊ฒ์ฆ
+ * - ์๋ฌ ์ฒ๋ฆฌ ๊ฒ์ฆ
+ */
+
+vi.mock("@/server/prisma", () => ({
+ prisma: {
+ brand: {
+ findMany: vi.fn(),
+ findUnique: vi.fn(),
+ },
+ },
+}));
+
+vi.mock("../../repositories/brand.repository", () => ({
+ brandDetailSelect: {
+ id: true,
+ nameEn: true,
+ nameKo: true,
+ description: true,
+ imageUrl: true,
+ brandUrl: true,
+ mapLocation: true,
+ },
+ parseMapLocation: vi.fn((location) => {
+ if (location === null) return null;
+ if (
+ typeof location === "object" &&
+ location !== null &&
+ !Array.isArray(location)
+ ) {
+ const obj = location as Record;
+ if (
+ typeof obj.latitude === "number" &&
+ typeof obj.longitude === "number"
+ ) {
+ return { latitude: obj.latitude, longitude: obj.longitude };
+ }
+ }
+ return null;
+ }),
+}));
+
+describe("Brand Service", () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ });
+
+ describe("getAllBrandsService", () => {
+ it("๋ชจ๋ ๋ธ๋๋๋ฅผ ์กฐํํด์ผ ํ๋ค", async () => {
+ const mockBrands = [
+ { id: "brand-1", nameEn: "Brand One", nameKo: "๋ธ๋๋ ์" },
+ { id: "brand-2", nameEn: "Brand Two", nameKo: null },
+ ];
+
+ vi.mocked(prisma.brand.findMany).mockResolvedValue(mockBrands as never);
+
+ const result = await getAllBrandsService();
+
+ expect(result.success).toBe(true);
+ if (result.success) {
+ expect(result.data).toHaveLength(2);
+ expect(result.data[0].nameKo).toBe("๋ธ๋๋ ์");
+ expect(result.data[1].nameKo).toBe("Brand Two"); // nameKo null โ nameEn ์ฌ์ฉ
+ }
+ });
+
+ it("DB ์๋ฌ ์ INTERNAL_ERROR๋ฅผ ๋ฐํํด์ผ ํ๋ค", async () => {
+ vi.mocked(prisma.brand.findMany).mockRejectedValue(
+ new Error("Database error"),
+ );
+
+ const result = await getAllBrandsService();
+
+ expect(result.success).toBe(false);
+ if (!result.success) {
+ expect(result.error).toBe("INTERNAL_ERROR");
+ }
+ });
+ });
+
+ describe("getBrandByIdService", () => {
+ it("ID๋ก ๋ธ๋๋๋ฅผ ์กฐํํด์ผ ํ๋ค", async () => {
+ const mockBrand = {
+ id: "brand-1",
+ nameEn: "Test Brand",
+ nameKo: "ํ
์คํธ ๋ธ๋๋",
+ description: "Test Description",
+ imageUrl: "test.jpg",
+ brandUrl: "https://test.com",
+ mapLocation: null,
+ };
+
+ vi.mocked(prisma.brand.findUnique).mockResolvedValue(mockBrand as never);
+
+ const result = await getBrandByIdService("brand-1");
+
+ expect(result.success).toBe(true);
+ if (result.success) {
+ expect(result.data.id).toBe("brand-1");
+ expect(result.data.nameKo).toBe("ํ
์คํธ ๋ธ๋๋");
+ }
+ });
+
+ it("nameKo๊ฐ null์ด๋ฉด nameEn์ ์ฌ์ฉํด์ผ ํ๋ค", async () => {
+ const mockBrand = {
+ id: "brand-1",
+ nameEn: "Test Brand",
+ nameKo: null,
+ description: "Test Description",
+ imageUrl: "test.jpg",
+ brandUrl: null,
+ mapLocation: null,
+ };
+
+ vi.mocked(prisma.brand.findUnique).mockResolvedValue(mockBrand as never);
+
+ const result = await getBrandByIdService("brand-1");
+
+ expect(result.success).toBe(true);
+ if (result.success) {
+ expect(result.data.nameKo).toBe("Test Brand");
+ }
+ });
+
+ it("mapLocation์ด ์์ผ๋ฉด ํ์ฑํ์ฌ ๋ฐํํด์ผ ํ๋ค", async () => {
+ const { parseMapLocation } =
+ await import("../../repositories/brand.repository");
+ const mockBrand = {
+ id: "brand-1",
+ nameEn: "Test Brand",
+ nameKo: "ํ
์คํธ ๋ธ๋๋",
+ description: null,
+ imageUrl: null,
+ brandUrl: null,
+ mapLocation: { latitude: 37.5665, longitude: 126.978 },
+ };
+
+ vi.mocked(prisma.brand.findUnique).mockResolvedValue(mockBrand as never);
+
+ await getBrandByIdService("brand-1");
+
+ expect(parseMapLocation).toHaveBeenCalledWith(mockBrand.mapLocation);
+ });
+
+ it("์กด์ฌํ์ง ์๋ ๋ธ๋๋๋ NOT_FOUND ์๋ฌ๋ฅผ ๋ฐํํด์ผ ํ๋ค", async () => {
+ vi.mocked(prisma.brand.findUnique).mockResolvedValue(null);
+
+ const result = await getBrandByIdService("non-existent");
+
+ expect(result.success).toBe(false);
+ if (!result.success) {
+ expect(result.error).toBe("NOT_FOUND");
+ }
+ });
+
+ it("DB ์๋ฌ ์ INTERNAL_ERROR๋ฅผ ๋ฐํํด์ผ ํ๋ค", async () => {
+ vi.mocked(prisma.brand.findUnique).mockRejectedValue(
+ new Error("Database error"),
+ );
+
+ const result = await getBrandByIdService("brand-1");
+
+ expect(result.success).toBe(false);
+ if (!result.success) {
+ expect(result.error).toBe("INTERNAL_ERROR");
+ }
+ });
+ });
+
+ describe("getBrandByNameService", () => {
+ it("์ด๋ฆ์ผ๋ก ๋ธ๋๋๋ฅผ ์กฐํํด์ผ ํ๋ค", async () => {
+ const mockBrand = {
+ id: "brand-1",
+ nameEn: "Test Brand",
+ nameKo: "ํ
์คํธ ๋ธ๋๋",
+ description: "Test Description",
+ imageUrl: "test.jpg",
+ brandUrl: "https://test.com",
+ mapLocation: null,
+ };
+
+ vi.mocked(prisma.brand.findUnique).mockResolvedValue(mockBrand as never);
+
+ const result = await getBrandByNameService("ํ
์คํธ ๋ธ๋๋");
+
+ expect(result.success).toBe(true);
+ if (result.success) {
+ expect(result.data.nameEn).toBe("Test Brand");
+ }
+ });
+
+ it("์กด์ฌํ์ง ์๋ ๋ธ๋๋๋ NOT_FOUND ์๋ฌ๋ฅผ ๋ฐํํด์ผ ํ๋ค", async () => {
+ vi.mocked(prisma.brand.findUnique).mockResolvedValue(null);
+
+ const result = await getBrandByNameService("Non Existent Brand");
+
+ expect(result.success).toBe(false);
+ if (!result.success) {
+ expect(result.error).toBe("NOT_FOUND");
+ }
+ });
+
+ it("DB ์๋ฌ ์ INTERNAL_ERROR๋ฅผ ๋ฐํํด์ผ ํ๋ค", async () => {
+ vi.mocked(prisma.brand.findUnique).mockRejectedValue(
+ new Error("Database error"),
+ );
+
+ const result = await getBrandByNameService("ํ
์คํธ ๋ธ๋๋");
+
+ expect(result.success).toBe(false);
+ if (!result.success) {
+ expect(result.error).toBe("INTERNAL_ERROR");
+ }
+ });
+ });
+
+ describe("getStoresByNameService", () => {
+ const perfumeStoreDocs = [
+ {
+ place_name: "๋ฅํฐํฌ ๋ช
๋",
+ address_name: "์์ธ ์ค๊ตฌ ๋ช
๋๊ธธ 1",
+ road_address_name: "์์ธ ์ค๊ตฌ ๋ช
๋๊ธธ 1",
+ phone: "02-1234-5678",
+ x: "126.978",
+ y: "37.5665",
+ category_name: "๊ฐ์ ,์ํ > ํฅ์",
+ category_group_code: "HP8",
+ distance: "500",
+ },
+ {
+ place_name: "๋ฅํฐํฌ ๊ฐ๋จ",
+ address_name: "์์ธ ๊ฐ๋จ๊ตฌ ์๊ตฌ์ ๋ก 1",
+ road_address_name: "์์ธ ๊ฐ๋จ๊ตฌ ์๊ตฌ์ ๋ก 1",
+ phone: "02-9876-5432",
+ x: "127.054",
+ y: "37.394",
+ category_name: "ํ์ฅํ",
+ category_group_code: "HP8",
+ distance: "",
+ },
+ ];
+
+ const makeKakaoResponse = (documents: typeof perfumeStoreDocs) => ({
+ meta: { total_count: documents.length },
+ documents,
+ });
+
+ beforeEach(() => {
+ process.env.KAKAO_REST_KEY = "test-kakao-key";
+
+ vi.stubGlobal(
+ "fetch",
+ vi.fn().mockResolvedValue({
+ ok: true,
+ json: vi.fn().mockResolvedValue(makeKakaoResponse(perfumeStoreDocs)),
+ }),
+ );
+ });
+
+ afterEach(() => {
+ vi.unstubAllGlobals();
+ delete process.env.KAKAO_REST_KEY;
+ });
+
+ it("์นด์นด์ค ๋ก์ปฌ ๊ฒ์ API๋ฅผ ํธ์ถํ๊ณ ๋งค์ฅ ๋ชฉ๋ก์ ๋ฐํํด์ผ ํ๋ค", async () => {
+ const result = await getStoresByNameService("๋ฅํฐํฌ");
+
+ expect(result.success).toBe(true);
+ if (result.success) {
+ expect(result.data.stores).toHaveLength(2);
+ expect(result.data.total).toBe(2);
+ expect(result.data.stores[0].name).toBe("๋ฅํฐํฌ ๋ช
๋");
+ }
+ });
+
+ it("x, y ์ขํ๊ฐ ์ ๊ณต๋๋ฉด radius ํ๋ผ๋ฏธํฐ๋ฅผ ํฌํจํ์ฌ API๋ฅผ ํธ์ถํด์ผ ํ๋ค", async () => {
+ await getStoresByNameService("๋ฅํฐํฌ", "127.054", "37.394");
+
+ const fetchCall = vi.mocked(fetch).mock.calls[0];
+ const url = fetchCall[0] as string;
+
+ expect(url).toContain("x=127.054");
+ expect(url).toContain("y=37.394");
+ expect(url).toContain("radius=20000");
+ });
+
+ it("x, y ์ขํ๊ฐ ์์ผ๋ฉด radius ํ๋ผ๋ฏธํฐ๋ฅผ ํฌํจํ์ง ์์์ผ ํ๋ค", async () => {
+ await getStoresByNameService("๋ฅํฐํฌ");
+
+ const fetchCall = vi.mocked(fetch).mock.calls[0];
+ const url = fetchCall[0] as string;
+
+ expect(url).not.toContain("radius");
+ });
+
+ it("์นดํ
๊ณ ๋ฆฌ ํํฐ๋ง โ '๊ฐ์ ,์ํ' + 'ํฅ์' ์กฐํฉ์ด ํฌํจ๋ ๋งค์ฅ์ ๋ฐํํด์ผ ํ๋ค", async () => {
+ const docs = [
+ {
+ ...perfumeStoreDocs[0],
+ category_name: "๊ฐ์ ,์ํ > ํฅ์ > ๋์นํฅ์",
+ },
+ {
+ ...perfumeStoreDocs[0],
+ place_name: "ํํฐ ์ ์ธ ๋งค์ฅ",
+ category_name: "์์์ > ์นดํ",
+ },
+ ];
+
+ vi.mocked(fetch).mockResolvedValue({
+ ok: true,
+ json: vi.fn().mockResolvedValue(makeKakaoResponse(docs)),
+ } as never);
+
+ const result = await getStoresByNameService("๋ฅํฐํฌ");
+
+ expect(result.success).toBe(true);
+ if (result.success) {
+ expect(result.data.stores).toHaveLength(1);
+ expect(result.data.stores[0].name).toBe("๋ฅํฐํฌ ๋ช
๋");
+ }
+ });
+
+ it("์นดํ
๊ณ ๋ฆฌ ํํฐ๋ง โ 'ํ์ฅํ'์ด ํฌํจ๋ ๋งค์ฅ์ ๋ฐํํด์ผ ํ๋ค", async () => {
+ const docs = [
+ {
+ ...perfumeStoreDocs[0],
+ place_name: "ํ์ฅํ ๋งค์ฅ",
+ category_name: "์ผํ > ํ์ฅํ",
+ },
+ {
+ ...perfumeStoreDocs[0],
+ place_name: "ํํฐ ์ ์ธ ๋งค์ฅ",
+ category_name: "์์์ > ๋ ์คํ ๋",
+ },
+ ];
+
+ vi.mocked(fetch).mockResolvedValue({
+ ok: true,
+ json: vi.fn().mockResolvedValue(makeKakaoResponse(docs)),
+ } as never);
+
+ const result = await getStoresByNameService("๋ฅํฐํฌ");
+
+ expect(result.success).toBe(true);
+ if (result.success) {
+ expect(result.data.stores).toHaveLength(1);
+ expect(result.data.stores[0].name).toBe("ํ์ฅํ ๋งค์ฅ");
+ }
+ });
+
+ it("distance๊ฐ ๋น ๋ฌธ์์ด์ด๋ฉด undefined๋ก ๋ณํํด์ผ ํ๋ค", async () => {
+ const result = await getStoresByNameService("๋ฅํฐํฌ");
+
+ expect(result.success).toBe(true);
+ if (result.success) {
+ const gangnะฐะผ = result.data.stores.find(
+ (s) => s.name === "๋ฅํฐํฌ ๊ฐ๋จ",
+ );
+ expect(gangnะฐะผ?.distance).toBeUndefined();
+ }
+ });
+
+ it("distance๊ฐ ์์ผ๋ฉด ์ซ์๋ก ๋ณํํด์ผ ํ๋ค", async () => {
+ const result = await getStoresByNameService("๋ฅํฐํฌ");
+
+ expect(result.success).toBe(true);
+ if (result.success) {
+ const myeongdong = result.data.stores.find(
+ (s) => s.name === "๋ฅํฐํฌ ๋ช
๋",
+ );
+ expect(myeongdong?.distance).toBe(500);
+ }
+ });
+
+ it("๊ฒ์ ๊ฒฐ๊ณผ๊ฐ ์์ผ๋ฉด ๋น ๋ฐฐ์ด์ ๋ฐํํด์ผ ํ๋ค", async () => {
+ vi.mocked(fetch).mockResolvedValue({
+ ok: true,
+ json: vi.fn().mockResolvedValue(makeKakaoResponse([])),
+ } as never);
+
+ const result = await getStoresByNameService("์กด์ฌํ์ง์๋๋ธ๋๋");
+
+ expect(result.success).toBe(true);
+ if (result.success) {
+ expect(result.data.stores).toHaveLength(0);
+ expect(result.data.total).toBe(0);
+ }
+ });
+
+ it("์นด์นด์ค API ์๋ต ์ค๋ฅ ์ INTERNAL_ERROR๋ฅผ ๋ฐํํด์ผ ํ๋ค", async () => {
+ vi.mocked(fetch).mockResolvedValue({
+ ok: false,
+ status: 401,
+ json: vi.fn().mockResolvedValue({ msg: "unauthorized" }),
+ } as never);
+
+ const result = await getStoresByNameService("๋ฅํฐํฌ");
+
+ expect(result.success).toBe(false);
+ if (!result.success) {
+ expect(result.error).toBe("INTERNAL_ERROR");
+ }
+ });
+
+ it("๋คํธ์ํฌ ์๋ฌ ์ INTERNAL_ERROR๋ฅผ ๋ฐํํด์ผ ํ๋ค", async () => {
+ vi.mocked(fetch).mockRejectedValue(new Error("Network error"));
+
+ const result = await getStoresByNameService("๋ฅํฐํฌ");
+
+ expect(result.success).toBe(false);
+ if (!result.success) {
+ expect(result.error).toBe("INTERNAL_ERROR");
+ }
+ });
+ });
+});
diff --git a/src/lib/hono/services/__tests__/comment.service.test.ts b/src/server/hono/services/__tests__/comment.service.test.ts
similarity index 88%
rename from src/lib/hono/services/__tests__/comment.service.test.ts
rename to src/server/hono/services/__tests__/comment.service.test.ts
index e0273e7d..52a13d42 100644
--- a/src/lib/hono/services/__tests__/comment.service.test.ts
+++ b/src/server/hono/services/__tests__/comment.service.test.ts
@@ -1,11 +1,12 @@
import { describe, it, expect, beforeEach, vi } from "vitest";
-import { prisma } from "@/lib/prisma";
+import { prisma } from "@/server/prisma";
import type { PrismaClient } from "@prisma/client";
import {
createCommentService,
updateCommentService,
deleteCommentService,
getPaginatedCommentService,
+ getCommentService,
} from "../comment.service";
import { DELETED_COMMENT_MESSAGE_BY_USER } from "../../schemas/comment.schema";
import { getTestData } from "./helpers/comment.test.helpers";
@@ -27,7 +28,7 @@ type PrismaTransactionClient = Omit<
*/
// Mock prisma
-vi.mock("@/lib/prisma", () => ({
+vi.mock("@/server/prisma", () => ({
prisma: {
user: {
findUnique: vi.fn(),
@@ -502,4 +503,60 @@ describe("Comment Service", () => {
}
});
});
+
+ describe("getCommentService", () => {
+ it("๊ฒ์๊ธ์ ์ ์ฒด ๋๊ธ ๋ชฉ๋ก์ ๋ฐํํด์ผ ํ๋ค", async () => {
+ const { ids, mockPost, mockComments } = getTestData();
+
+ vi.mocked(prisma.post.findUnique).mockResolvedValue(mockPost as never);
+ vi.mocked(prisma.comment.findMany).mockResolvedValue(mockComments(3) as never);
+
+ const result = await getCommentService(ids.postId);
+
+ expect(result.success).toBe(true);
+ if (result.success) {
+ expect(result.data).toHaveLength(3);
+ }
+ });
+
+ it("parentId=null ์กฐ๊ฑด์ผ๋ก ์ต์์ ๋๊ธ๋ง ์กฐํํด์ผ ํ๋ค", async () => {
+ const { ids, mockPost, mockComments } = getTestData();
+
+ vi.mocked(prisma.post.findUnique).mockResolvedValue(mockPost as never);
+ vi.mocked(prisma.comment.findMany).mockResolvedValue(mockComments(2) as never);
+
+ await getCommentService(ids.postId);
+
+ expect(prisma.comment.findMany).toHaveBeenCalledWith(
+ expect.objectContaining({
+ where: { postId: ids.postId, parentId: null },
+ })
+ );
+ });
+
+ it("์กด์ฌํ์ง ์๋ postId์ด๋ฉด NOT_FOUND๋ฅผ ๋ฐํํด์ผ ํ๋ค", async () => {
+ vi.mocked(prisma.post.findUnique).mockResolvedValue(null);
+
+ const result = await getCommentService("non-existent-post");
+
+ expect(result.success).toBe(false);
+ if (!result.success) {
+ expect(result.error).toBe("NOT_FOUND");
+ }
+ });
+
+ it("DB ์๋ฌ ์ INTERNAL_ERROR๋ฅผ ๋ฐํํด์ผ ํ๋ค", async () => {
+ const { ids, mockPost } = getTestData();
+
+ vi.mocked(prisma.post.findUnique).mockResolvedValue(mockPost as never);
+ vi.mocked(prisma.comment.findMany).mockRejectedValue(new Error("Database error"));
+
+ const result = await getCommentService(ids.postId);
+
+ expect(result.success).toBe(false);
+ if (!result.success) {
+ expect(result.error).toBe("INTERNAL_ERROR");
+ }
+ });
+ });
});
diff --git a/src/lib/hono/services/__tests__/community.service.test.ts b/src/server/hono/services/__tests__/community.service.test.ts
similarity index 99%
rename from src/lib/hono/services/__tests__/community.service.test.ts
rename to src/server/hono/services/__tests__/community.service.test.ts
index 50ae6540..f9c85c8e 100644
--- a/src/lib/hono/services/__tests__/community.service.test.ts
+++ b/src/server/hono/services/__tests__/community.service.test.ts
@@ -1,5 +1,5 @@
import { describe, it, expect, beforeEach, vi } from "vitest";
-import { prisma } from "@/lib/prisma";
+import { prisma } from "@/server/prisma";
import type { PrismaClient, PostCategory } from "@prisma/client";
import {
createPostService,
@@ -9,7 +9,7 @@ import {
togglePostLikeService,
togglePostBookmarkService,
getPaginatedPostListService,
-} from "../community.service";
+} from "../community";
import { getTestData } from "./helpers/community.test.helpers";
// Prisma Interactive Transaction ํ์
์ ์
@@ -29,7 +29,7 @@ type PrismaTransactionClient = Omit<
*/
// Mock prisma
-vi.mock("@/lib/prisma", () => ({
+vi.mock("@/server/prisma", () => ({
prisma: {
user: {
findUnique: vi.fn(),
diff --git a/src/lib/hono/services/__tests__/consecutiveLogin.service.test.ts b/src/server/hono/services/__tests__/consecutiveLogin.service.test.ts
similarity index 98%
rename from src/lib/hono/services/__tests__/consecutiveLogin.service.test.ts
rename to src/server/hono/services/__tests__/consecutiveLogin.service.test.ts
index da2a8469..84a52535 100644
--- a/src/lib/hono/services/__tests__/consecutiveLogin.service.test.ts
+++ b/src/server/hono/services/__tests__/consecutiveLogin.service.test.ts
@@ -1,6 +1,6 @@
import { describe, it, expect, beforeEach, vi } from "vitest";
-import { processConsecutiveLoginService } from "../point.service";
-import { prisma } from "@/lib/prisma";
+import { processConsecutiveLoginService } from "../point";
+import { prisma } from "@/server/prisma";
import dayjs from "dayjs";
/**
@@ -34,7 +34,7 @@ const getTestDates = (baseDate = "2025-11-15") => ({
});
// Mock prisma
-vi.mock("@/lib/prisma", () => ({
+vi.mock("@/server/prisma", () => ({
prisma: {
user: {
findUnique: vi.fn(),
diff --git a/src/server/hono/services/__tests__/draft.service.test.ts b/src/server/hono/services/__tests__/draft.service.test.ts
new file mode 100644
index 00000000..f56f265a
--- /dev/null
+++ b/src/server/hono/services/__tests__/draft.service.test.ts
@@ -0,0 +1,424 @@
+import { describe, it, expect, beforeEach, vi } from "vitest";
+import { prisma } from "@/server/prisma";
+import { DraftType } from "@prisma/client";
+import {
+ createOrUpdateDraftService,
+ getDraftService,
+ listDraftsService,
+ deleteDraftService,
+} from "../draft.service";
+import { getTestData } from "./helpers/draft.test.helpers";
+import { checkResourceExists } from "../../repositories/base.repository";
+
+/**
+ * Draft ์๋น์ค ํ
์คํธ
+ *
+ * ํ
์คํธ ์ ๋ต:
+ * - CREATE/UPDATE ํ์
๋ถ๊ธฐ ๊ฒ์ฆ
+ * - ์์ ๊ถ ํ์ธ (FORBIDDEN)
+ * - perfumeIds ๊ธฐ๋ฐ ํฅ์ ์กฐํ (batch ์ต์ ํ ํฌํจ)
+ * - ์๋ฌ ์ฒ๋ฆฌ ๊ฒ์ฆ
+ */
+
+vi.mock("@/server/prisma", () => ({
+ prisma: {
+ postDraft: {
+ upsert: vi.fn(),
+ findUnique: vi.fn(),
+ findMany: vi.fn(),
+ delete: vi.fn(),
+ },
+ perfume: {
+ findMany: vi.fn(),
+ },
+ },
+}));
+
+vi.mock("../../repositories/base.repository", () => ({
+ checkResourceExists: vi.fn(),
+}));
+
+describe("Draft Service", () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ });
+
+ describe("createOrUpdateDraftService", () => {
+ describe("CREATE ํ์
", () => {
+ it("CREATE ํ์
์์ ์ ์ฅ์ ์์ฑํ๊ณ ํฅ์ ์ ๋ณด๋ฅผ ๋ฐํํด์ผ ํ๋ค", async () => {
+ const { ids, createDraftPayload, mockDraft, mockPerfumeRecord } = getTestData();
+ vi.mocked(checkResourceExists).mockResolvedValue({ success: true, data: true });
+ vi.mocked(prisma.postDraft.upsert).mockResolvedValue(mockDraft() as never);
+ vi.mocked(prisma.perfume.findMany).mockResolvedValue([
+ mockPerfumeRecord(ids.perfumeId1),
+ ] as never);
+
+ const result = await createOrUpdateDraftService(createDraftPayload());
+
+ expect(result.success).toBe(true);
+ if (result.success) {
+ expect(result.data.type).toBe(DraftType.CREATE);
+ expect(result.data.perfumes).toHaveLength(1);
+ expect(result.data.perfumes![0].nameKo).toBe("ํ
์คํธ ํฅ์");
+ expect(result.data.createdAt).toMatch(/^\d{4}-\d{2}-\d{2}T/); // ISO string
+ }
+ });
+
+ it("perfumeIds๊ฐ ๋น ๋ฐฐ์ด์ด์ด๋ ์ ์ ์์ฑ๋์ด์ผ ํ๋ค", async () => {
+ const { createDraftPayload, mockDraft } = getTestData();
+ const emptyPerfumeDraft = { ...mockDraft(), perfumeIds: [] };
+ vi.mocked(checkResourceExists).mockResolvedValue({ success: true, data: true });
+ vi.mocked(prisma.postDraft.upsert).mockResolvedValue(emptyPerfumeDraft as never);
+ vi.mocked(prisma.perfume.findMany).mockResolvedValue([] as never);
+
+ const result = await createOrUpdateDraftService({
+ ...createDraftPayload(),
+ perfumeIds: [],
+ });
+
+ expect(result.success).toBe(true);
+ if (result.success) {
+ expect(result.data.perfumes).toHaveLength(0);
+ }
+ });
+
+ it("CREATE ํ์
์ postId๊ฐ ์์ด๋ null๋ก ์ ์ฅ๋์ด์ผ ํ๋ค", async () => {
+ const { ids, createDraftPayload, mockDraft, mockPerfumeRecord } = getTestData();
+ vi.mocked(checkResourceExists).mockResolvedValue({ success: true, data: true });
+ vi.mocked(prisma.postDraft.upsert).mockResolvedValue(mockDraft() as never);
+ vi.mocked(prisma.perfume.findMany).mockResolvedValue([
+ mockPerfumeRecord(ids.perfumeId1),
+ ] as never);
+
+ await createOrUpdateDraftService({
+ ...createDraftPayload(),
+ });
+
+ // upsert ํธ์ถ ์ CREATE ํ์
์ postId๋ null์ด์ด์ผ ํจ
+ expect(prisma.postDraft.upsert).toHaveBeenCalledWith(
+ expect.objectContaining({
+ create: expect.objectContaining({ postId: null }),
+ update: expect.objectContaining({ postId: null }),
+ })
+ );
+ });
+ });
+
+ describe("UPDATE ํ์
", () => {
+ it("UPDATE ํ์
์ postId๊ฐ ์์ผ๋ฉด BAD_REQUEST๋ฅผ ๋ฐํํด์ผ ํ๋ค", async () => {
+ const { updateDraftPayload } = getTestData();
+ vi.mocked(checkResourceExists).mockResolvedValue({ success: true, data: true });
+
+ const result = await createOrUpdateDraftService({
+ ...updateDraftPayload(),
+ postId: undefined,
+ });
+
+ expect(result.success).toBe(false);
+ if (!result.success) {
+ expect(result.error).toBe("BAD_REQUEST");
+ }
+ });
+
+ it("UPDATE ํ์
์ postId์ ํจ๊ป ์์ ์ ์ฅ์ ์์ฑํด์ผ ํ๋ค", async () => {
+ const { ids, updateDraftPayload, mockUpdateDraft, mockPerfumeRecord } = getTestData();
+ vi.mocked(checkResourceExists).mockResolvedValue({ success: true, data: true });
+ vi.mocked(prisma.postDraft.upsert).mockResolvedValue(mockUpdateDraft() as never);
+ vi.mocked(prisma.perfume.findMany).mockResolvedValue([
+ mockPerfumeRecord(ids.perfumeId1),
+ mockPerfumeRecord(ids.perfumeId2),
+ ] as never);
+
+ const result = await createOrUpdateDraftService(updateDraftPayload());
+
+ expect(result.success).toBe(true);
+ if (result.success) {
+ expect(result.data.type).toBe(DraftType.UPDATE);
+ expect(result.data.postId).toBe(ids.postId);
+ }
+ });
+
+ it("UPDATE ํ์
์์ ์กด์ฌํ์ง ์๋ postId์ด๋ฉด NOT_FOUND๋ฅผ ๋ฐํํด์ผ ํ๋ค", async () => {
+ const { updateDraftPayload } = getTestData();
+ vi.mocked(checkResourceExists)
+ .mockResolvedValueOnce({ success: true, data: true }) // user ์กด์ฌ
+ .mockResolvedValueOnce({
+ success: false,
+ error: "NOT_FOUND",
+ message: "๊ฒ์๊ธ์(๋ฅผ) ์ฐพ์ ์ ์์ต๋๋ค.",
+ }); // post ๋ฏธ์กด์ฌ
+
+ const result = await createOrUpdateDraftService(updateDraftPayload());
+
+ expect(result.success).toBe(false);
+ if (!result.success) {
+ expect(result.error).toBe("NOT_FOUND");
+ }
+ });
+ });
+
+ describe("์๋ฌ ์ฒ๋ฆฌ", () => {
+ it("์กด์ฌํ์ง ์๋ userId์ด๋ฉด NOT_FOUND๋ฅผ ๋ฐํํด์ผ ํ๋ค", async () => {
+ const { createDraftPayload } = getTestData();
+ vi.mocked(checkResourceExists).mockResolvedValue({
+ success: false,
+ error: "NOT_FOUND",
+ message: "์ฌ์ฉ์์(๋ฅผ) ์ฐพ์ ์ ์์ต๋๋ค.",
+ });
+
+ const result = await createOrUpdateDraftService(createDraftPayload());
+
+ expect(result.success).toBe(false);
+ if (!result.success) {
+ expect(result.error).toBe("NOT_FOUND");
+ }
+ });
+
+ it("DB ์๋ฌ ์ INTERNAL_ERROR๋ฅผ ๋ฐํํด์ผ ํ๋ค", async () => {
+ const { createDraftPayload } = getTestData();
+ vi.mocked(checkResourceExists).mockResolvedValue({ success: true, data: true });
+ vi.mocked(prisma.postDraft.upsert).mockRejectedValue(new Error("Database error"));
+
+ const result = await createOrUpdateDraftService(createDraftPayload());
+
+ expect(result.success).toBe(false);
+ if (!result.success) {
+ expect(result.error).toBe("INTERNAL_ERROR");
+ }
+ });
+ });
+ });
+
+ describe("getDraftService", () => {
+ it("์์ ์ ์ฅ์ ์กฐํํ๊ณ ํฅ์ ์ ๋ณด๋ฅผ ํฌํจํ์ฌ ๋ฐํํด์ผ ํ๋ค", async () => {
+ const { ids, mockDraft, mockPerfumeRecord } = getTestData();
+
+ vi.mocked(prisma.postDraft.findUnique).mockResolvedValue(mockDraft() as never);
+ vi.mocked(prisma.perfume.findMany).mockResolvedValue([
+ mockPerfumeRecord(ids.perfumeId1),
+ ] as never);
+
+ const result = await getDraftService(ids.draftId, ids.userId);
+
+ expect(result.success).toBe(true);
+ if (result.success) {
+ expect(result.data.id).toBe(ids.draftId);
+ expect(result.data.perfumes).toHaveLength(1);
+ expect(result.data.updatedAt).toMatch(/^\d{4}-\d{2}-\d{2}T/);
+ }
+ });
+
+ it("perfumeIds ๊ธฐ๋ฐ์ผ๋ก ํฅ์ ์ ๋ณด๋ฅผ ์กฐํํด์ผ ํ๋ค", async () => {
+ const { ids, mockDraft, mockPerfumeRecord } = getTestData();
+
+ vi.mocked(prisma.postDraft.findUnique).mockResolvedValue(mockDraft() as never);
+ vi.mocked(prisma.perfume.findMany).mockResolvedValue([
+ mockPerfumeRecord(ids.perfumeId1),
+ ] as never);
+
+ await getDraftService(ids.draftId, ids.userId);
+
+ expect(prisma.perfume.findMany).toHaveBeenCalledWith(
+ expect.objectContaining({
+ where: { id: { in: [ids.perfumeId1] } },
+ })
+ );
+ });
+
+ it("์กด์ฌํ์ง ์๋ draft์ด๋ฉด NOT_FOUND๋ฅผ ๋ฐํํด์ผ ํ๋ค", async () => {
+ const { ids } = getTestData();
+
+ vi.mocked(prisma.postDraft.findUnique).mockResolvedValue(null);
+
+ const result = await getDraftService(ids.draftId, ids.userId);
+
+ expect(result.success).toBe(false);
+ if (!result.success) {
+ expect(result.error).toBe("NOT_FOUND");
+ }
+ });
+
+ it("๋ค๋ฅธ ์ฌ์ฉ์์ draft ์กฐํ ์ FORBIDDEN์ ๋ฐํํด์ผ ํ๋ค", async () => {
+ const { ids, mockDraft } = getTestData();
+
+ vi.mocked(prisma.postDraft.findUnique).mockResolvedValue({
+ ...mockDraft(),
+ userId: "other-user-id",
+ } as never);
+
+ const result = await getDraftService(ids.draftId, ids.userId);
+
+ expect(result.success).toBe(false);
+ if (!result.success) {
+ expect(result.error).toBe("FORBIDDEN");
+ }
+ });
+
+ it("DB ์๋ฌ ์ INTERNAL_ERROR๋ฅผ ๋ฐํํด์ผ ํ๋ค", async () => {
+ const { ids } = getTestData();
+
+ vi.mocked(prisma.postDraft.findUnique).mockRejectedValue(new Error("Database error"));
+
+ const result = await getDraftService(ids.draftId, ids.userId);
+
+ expect(result.success).toBe(false);
+ if (!result.success) {
+ expect(result.error).toBe("INTERNAL_ERROR");
+ }
+ });
+ });
+
+ describe("listDraftsService", () => {
+ it("์ฌ์ฉ์์ ๋ชจ๋ ์์ ์ ์ฅ์ ์ต์ ์์ผ๋ก ๋ฐํํด์ผ ํ๋ค", async () => {
+ const { ids, mockDraftList, mockPerfumeRecord } = getTestData();
+
+ vi.mocked(prisma.postDraft.findMany).mockResolvedValue(mockDraftList() as never);
+ vi.mocked(prisma.perfume.findMany).mockResolvedValue([
+ mockPerfumeRecord(ids.perfumeId1),
+ mockPerfumeRecord(ids.perfumeId2),
+ ] as never);
+
+ const result = await listDraftsService(ids.userId);
+
+ expect(result.success).toBe(true);
+ if (result.success) {
+ expect(result.data).toHaveLength(2);
+ expect(prisma.postDraft.findMany).toHaveBeenCalledWith(
+ expect.objectContaining({
+ where: { userId: ids.userId },
+ orderBy: { updatedAt: "desc" },
+ })
+ );
+ }
+ });
+
+ it("์ฌ๋ฌ draft์ perfumeIds๋ฅผ ํ ๋ฒ์ batch ์กฐํํด์ผ ํ๋ค", async () => {
+ const { ids, mockDraftList, mockPerfumeRecord } = getTestData();
+
+ vi.mocked(prisma.postDraft.findMany).mockResolvedValue(mockDraftList() as never);
+ vi.mocked(prisma.perfume.findMany).mockResolvedValue([
+ mockPerfumeRecord(ids.perfumeId1),
+ mockPerfumeRecord(ids.perfumeId2),
+ ] as never);
+
+ await listDraftsService(ids.userId);
+
+ // ๋ draft์ perfumeIds๋ฅผ ํ ๋ฒ์ ์ฟผ๋ฆฌ๋ก ์กฐํ
+ expect(prisma.perfume.findMany).toHaveBeenCalledOnce();
+ expect(prisma.perfume.findMany).toHaveBeenCalledWith(
+ expect.objectContaining({
+ where: {
+ id: {
+ in: expect.arrayContaining([ids.perfumeId1, ids.perfumeId2]),
+ },
+ },
+ })
+ );
+ });
+
+ it("์์ ์ ์ฅ์ด ์์ผ๋ฉด ๋น ๋ฐฐ์ด์ ๋ฐํํด์ผ ํ๋ค", async () => {
+ const { ids } = getTestData();
+
+ vi.mocked(prisma.postDraft.findMany).mockResolvedValue([] as never);
+
+ const result = await listDraftsService(ids.userId);
+
+ expect(result.success).toBe(true);
+ if (result.success) {
+ expect(result.data).toHaveLength(0);
+ // perfume ์กฐํ๊ฐ ํธ์ถ๋์ง ์์์ผ ํจ
+ expect(prisma.perfume.findMany).not.toHaveBeenCalled();
+ }
+ });
+
+ it("perfumeIds๊ฐ ์๋ draft๋ ๋น perfumes ๋ฐฐ์ด์ ๋ฐํํด์ผ ํ๋ค", async () => {
+ const { ids, mockDraft } = getTestData();
+ const draftWithNoPerfumes = { ...mockDraft(), perfumeIds: [] };
+
+ vi.mocked(prisma.postDraft.findMany).mockResolvedValue([draftWithNoPerfumes] as never);
+
+ const result = await listDraftsService(ids.userId);
+
+ expect(result.success).toBe(true);
+ if (result.success && Array.isArray(result.data)) {
+ expect(result.data[0].perfumes).toHaveLength(0);
+ // perfumeIds๊ฐ ์์ผ๋ฏ๋ก perfume ์กฐํ๊ฐ ํธ์ถ๋์ง ์์์ผ ํจ
+ expect(prisma.perfume.findMany).not.toHaveBeenCalled();
+ }
+ });
+
+ it("DB ์๋ฌ ์ INTERNAL_ERROR๋ฅผ ๋ฐํํด์ผ ํ๋ค", async () => {
+ const { ids } = getTestData();
+
+ vi.mocked(prisma.postDraft.findMany).mockRejectedValue(new Error("Database error"));
+
+ const result = await listDraftsService(ids.userId);
+
+ expect(result.success).toBe(false);
+ if (!result.success) {
+ expect(result.error).toBe("INTERNAL_ERROR");
+ }
+ });
+ });
+
+ describe("deleteDraftService", () => {
+ it("์์ ํ ์์ ์ ์ฅ์ ์ญ์ ํด์ผ ํ๋ค", async () => {
+ const { ids, mockDraft } = getTestData();
+
+ vi.mocked(prisma.postDraft.findUnique).mockResolvedValue(mockDraft() as never);
+ vi.mocked(prisma.postDraft.delete).mockResolvedValue({} as never);
+
+ const result = await deleteDraftService(ids.draftId, ids.userId);
+
+ expect(result.success).toBe(true);
+ if (result.success) {
+ expect(result.data.message).toContain("์ญ์ ");
+ }
+ expect(prisma.postDraft.delete).toHaveBeenCalledWith({
+ where: { id: ids.draftId },
+ });
+ });
+
+ it("์กด์ฌํ์ง ์๋ draft ์ญ์ ์ NOT_FOUND๋ฅผ ๋ฐํํด์ผ ํ๋ค", async () => {
+ const { ids } = getTestData();
+
+ vi.mocked(prisma.postDraft.findUnique).mockResolvedValue(null);
+
+ const result = await deleteDraftService(ids.draftId, ids.userId);
+
+ expect(result.success).toBe(false);
+ if (!result.success) {
+ expect(result.error).toBe("NOT_FOUND");
+ }
+ });
+
+ it("๋ค๋ฅธ ์ฌ์ฉ์์ draft ์ญ์ ์ FORBIDDEN์ ๋ฐํํด์ผ ํ๋ค", async () => {
+ const { ids, mockDraft } = getTestData();
+
+ vi.mocked(prisma.postDraft.findUnique).mockResolvedValue({
+ ...mockDraft(),
+ userId: "other-user-id",
+ } as never);
+
+ const result = await deleteDraftService(ids.draftId, ids.userId);
+
+ expect(result.success).toBe(false);
+ if (!result.success) {
+ expect(result.error).toBe("FORBIDDEN");
+ }
+ });
+
+ it("DB ์๋ฌ ์ INTERNAL_ERROR๋ฅผ ๋ฐํํด์ผ ํ๋ค", async () => {
+ const { ids } = getTestData();
+
+ vi.mocked(prisma.postDraft.findUnique).mockRejectedValue(new Error("Database error"));
+
+ const result = await deleteDraftService(ids.draftId, ids.userId);
+
+ expect(result.success).toBe(false);
+ if (!result.success) {
+ expect(result.error).toBe("INTERNAL_ERROR");
+ }
+ });
+ });
+});
diff --git a/src/lib/hono/services/__tests__/earnPoints.service.test.ts b/src/server/hono/services/__tests__/earnPoints.service.test.ts
similarity index 72%
rename from src/lib/hono/services/__tests__/earnPoints.service.test.ts
rename to src/server/hono/services/__tests__/earnPoints.service.test.ts
index f7dfbce8..5a8746a4 100644
--- a/src/lib/hono/services/__tests__/earnPoints.service.test.ts
+++ b/src/server/hono/services/__tests__/earnPoints.service.test.ts
@@ -1,6 +1,6 @@
import { describe, it, expect, beforeEach, vi } from "vitest";
-import { earnPointsService } from "../point.service";
-import { prisma } from "@/lib/prisma";
+import { earnPointsService } from "../point";
+import { prisma } from "@/server/prisma";
import { PointActivityType } from "@prisma/client";
/**
@@ -29,7 +29,7 @@ interface PointHistoryCreateArgs {
}
// Mock prisma
-vi.mock("@/lib/prisma", () => ({
+vi.mock("@/server/prisma", () => ({
prisma: {
user: {
findUnique: vi.fn(),
@@ -49,96 +49,59 @@ describe("earnPointsService", () => {
vi.clearAllMocks();
});
- describe("์ ์ ํฌ์ธํธ ์ ๋ฆฝ", () => {
- it("๊ฒ์๋ฌผ ์์ฑ ์ 5ํฌ์ธํธ๊ฐ ์ ๋ฆฝ๋์ด์ผ ํ๋ค", async () => {
- (prisma.user.findUnique as ReturnType).mockResolvedValue({
- id: TEST_USER_ID,
- });
-
- (prisma.$transaction as ReturnType).mockImplementation(
- async (callback) => {
- await callback({
- pointHistory: { create: vi.fn() },
- user: {
- update: vi.fn().mockResolvedValue({ totalPoints: 105 }),
- },
- });
- return { totalPoints: 105 };
+ describe("ํ๋ ํ์
๋ณ ํฌ์ธํธ ๊ธ์ก ๊ฒ์ฆ", () => {
+ const testCases = [
+ {
+ activityType: PointActivityType.CREATE_POST,
+ expectedPoints: 5,
+ referenceId: "post-123",
+ label: "๊ฒ์๋ฌผ ์์ฑ",
+ },
+ {
+ activityType: PointActivityType.CREATE_COMMENT,
+ expectedPoints: 1,
+ referenceId: "comment-456",
+ label: "๋๊ธ ์์ฑ",
+ },
+ {
+ activityType: PointActivityType.LIKE_POST,
+ expectedPoints: 1,
+ referenceId: "post-789",
+ label: "์ข์์",
+ },
+ ];
+
+ it.each(testCases)(
+ "$label ์ $expectedPointsํฌ์ธํธ๊ฐ ์ ๋ฆฝ๋์ด์ผ ํ๋ค",
+ async ({ activityType, expectedPoints, referenceId }) => {
+ const initialPoints = 100;
+ const expectedTotal = initialPoints + expectedPoints;
+
+ (prisma.user.findUnique as ReturnType).mockResolvedValue({
+ id: TEST_USER_ID,
+ });
+
+ (prisma.$transaction as ReturnType