@@ -41,15 +79,26 @@ export default function ProfileCard() {
-
+
-
+
@@ -85,4 +134,59 @@ export default function ProfileCard() {
);
-}
\ No newline at end of file
+}
+
+type ProfileCardResult = {
+ nickname?: string;
+ profileImageUrl?: string | null;
+ gender?: string;
+ age?: number | null;
+ snsAccount?: string | null;
+ contentCategories?: string[] | null;
+ matchingResult?: {
+ creatorType?: string | null;
+ } | null;
+};
+
+type ProfileCardResponse = {
+ isSuccess: boolean;
+ code: string;
+ message: string;
+ result: ProfileCardResult;
+};
+
+type FeatureResult = {
+ beautyType?: {
+ skinType?: string[] | null;
+ skinBrightness?: string | null;
+ makeupStyle?: string[] | null;
+ interestCategories?: string[] | null;
+ interestFunctions?: string[] | null;
+ } | null;
+ fashionType?: {
+ height?: string | null;
+ bodyShape?: string | null;
+ topSize?: string | null;
+ bottomSize?: string | null;
+ interestFields?: string[] | null;
+ interestStyles?: string[] | null;
+ interestBrands?: string[] | null;
+ } | null;
+ contentsType?: {
+ viewerGender?: string[] | null;
+ viewerAge?: string[] | null;
+ avgVideoLength?: string | null;
+ avgViews?: string | null;
+ contentFormats?: string[] | null;
+ contentTones?: string[] | null;
+ desiredInvolvement?: string[] | null;
+ desiredUsageScope?: string[] | null;
+ } | null;
+};
+
+type FeatureResponse = {
+ isSuccess: boolean;
+ code: string;
+ message: string;
+ result: FeatureResult;
+};
diff --git a/app/routes/mypage/terms/route.tsx b/app/routes/mypage/terms/route.tsx
index e69de29b..e7146b33 100644
--- a/app/routes/mypage/terms/route.tsx
+++ b/app/routes/mypage/terms/route.tsx
@@ -0,0 +1,5 @@
+import MyPageTerms from "./terms-content";
+
+export default function TermsLayout() {
+ return
;
+}
diff --git a/app/routes/mypage/terms/terms-content.tsx b/app/routes/mypage/terms/terms-content.tsx
index 18a00db7..73d6a66e 100644
--- a/app/routes/mypage/terms/terms-content.tsx
+++ b/app/routes/mypage/terms/terms-content.tsx
@@ -1,3 +1,60 @@
+import { useNavigate } from "react-router";
+import NavigationHeader from "../../../components/common/NavigateHeader";
+import { useHideHeader } from "../../../hooks/useHideHeader";
+
+const TERMS = [
+ {
+ title: "제 1 조 (목적)",
+ body:
+ "본 이용약관은 리얼매치(이하 \"회사\")가 운영하는 인터넷 사이트 및 모바일 " +
+ "애플리케이션에서 제공하는 매칭 서비스(이하 \"서비스\")와 관련하여, 회사와 이용 고객(또는 \"회원\")간의 " +
+ "권리, 의무 및 책임사항, 회사와 회원 간의 서비스 이용조건 및 절차를 규정함을 목적으로 합니다.",
+ },
+ {
+ title: "제 2 조 (용어의 정의)",
+ body:
+ "본 약관에서 사용하는 용어의 정의는 다음과 같습니다.\n" +
+ "1. \"서비스\"란 \"회원\"이 컴퓨터, 휴대용 단말기 등 각종 유·무선 또는 프로그램을 통해 이용할 수 있도록 " +
+ "\"회사\"가 제공하는 모든 \"서비스\"를 의미합니다.\n" +
+ "2. \"회원\"이란 \"회사\"에 개인정보를 제공하여 회원등록을 한 자로서, 테이블링이 제공하는 \"서비스\"를 이용하는 사용자를 말합니다.\n" +
+ "3. \"비회원\"이란 회원가입 없이 \"회사\"가 제공하는 \"서비스\"를 이용하는 자를 말합니다.\n" +
+ "4. \"이용자\"란 \"서비스\"를 이용하는 자를 말하며, 회원과 비회원을 모두 포함합니다.\n" +
+ "5. \"게시물\"이란 \"회원\"이 \"서비스\"를 이용함에 있어 \"서비스\"상에 게시한 부호·문자·음성·음향 형태의 글, 사진, 동영상 및 각종 파일과 링크 등을 의미합니다.\n" +
+ "6. 리얼매치가 발행/관리하는...",
+ },
+];
+
export default function MyPageTerms() {
- return
Hello "/mypage/terms"!
+ useHideHeader(true);
+ const navigate = useNavigate();
+
+ return (
+
+
+
+ navigate(-1)} />
+
+
+
+
+
약관
+
+ {TERMS.map((section) => (
+
+
+ {section.title}
+
+
+ {section.body}
+
+
+ ))}
+
+
+
+
+ );
}
diff --git a/app/routes/mypage/traits/route.tsx b/app/routes/mypage/traits/route.tsx
new file mode 100644
index 00000000..12d34d01
--- /dev/null
+++ b/app/routes/mypage/traits/route.tsx
@@ -0,0 +1,5 @@
+import TraitsPage from "./traits-content";
+
+export default function TraitsPageLayout() {
+ return
;
+}
diff --git a/app/routes/mypage/traits/traits-content.tsx b/app/routes/mypage/traits/traits-content.tsx
new file mode 100644
index 00000000..246e1eeb
--- /dev/null
+++ b/app/routes/mypage/traits/traits-content.tsx
@@ -0,0 +1,291 @@
+import { useEffect, useMemo, useState } from "react";
+import { useNavigate } from "react-router";
+import NavigationHeader from "../../../components/common/NavigateHeader";
+import { useHideHeader } from "../../../hooks/useHideHeader";
+import { axiosInstance } from "../../../api/axios";
+import { TRAITS } from "../components/profileCard/traitData";
+
+type FeatureResult = {
+ beautyType?: {
+ skinType?: string[] | null;
+ skinBrightness?: string | null;
+ makeupStyle?: string[] | null;
+ interestCategories?: string[] | null;
+ interestFunctions?: string[] | null;
+ } | null;
+ fashionType?: {
+ height?: string | null;
+ bodyShape?: string | null;
+ topSize?: string | null;
+ bottomSize?: string | null;
+ interestFields?: string[] | null;
+ interestStyles?: string[] | null;
+ interestBrands?: string[] | null;
+ } | null;
+ contentsType?: {
+ viewerGender?: string[] | null;
+ viewerAge?: string[] | null;
+ avgVideoLength?: string | null;
+ avgViews?: string | null;
+ contentFormats?: string[] | null;
+ contentTones?: string[] | null;
+ desiredInvolvement?: string[] | null;
+ desiredUsageScope?: string[] | null;
+ } | null;
+};
+
+type FeatureResponse = {
+ isSuccess: boolean;
+ code: string;
+ message: string;
+ result: FeatureResult;
+};
+
+export default function TraitsPage() {
+ useHideHeader(true);
+ const navigate = useNavigate();
+ const [feature, setFeature] = useState
(null);
+
+ useEffect(() => {
+ let isMounted = true;
+
+ const fetchData = async () => {
+ try {
+ const featureRes = await axiosInstance.get(
+ "/api/v1/users/me/feature",
+ );
+ if (!isMounted) return;
+ setFeature(featureRes.data?.isSuccess ? featureRes.data.result : null);
+ } catch (error) {
+ console.error("특성 조회 실패:", error);
+ if (!isMounted) return;
+ setFeature(null);
+ }
+ };
+
+ fetchData();
+
+ return () => {
+ isMounted = false;
+ };
+ }, []);
+
+ const traits = useMemo(() => {
+ if (!feature) return TRAITS;
+
+ const beauty = feature.beautyType;
+ const fashion = feature.fashionType;
+ const content = feature.contentsType;
+
+ return TRAITS.map((trait) => {
+ if (trait.id === "beauty") {
+ return {
+ ...trait,
+ previewLines: [
+ { label: "피부 타입", value: (beauty?.skinType ?? []).join(", ") },
+ { label: "피부 밝기", value: beauty?.skinBrightness ?? "" },
+ {
+ label: "메이크업 \n스타일",
+ value: (beauty?.makeupStyle ?? []).join(", "),
+ },
+ ],
+ topSummary: [
+ { label: "피부타입", value: (beauty?.skinType ?? []).join(", ") },
+ { label: "피부 밝기", value: beauty?.skinBrightness ?? "" },
+ {
+ label: "메이크업 스타일",
+ value: (beauty?.makeupStyle ?? []).join(", "),
+ },
+ ],
+ sections: [
+ {
+ title: "관심 카테고리",
+ items: beauty?.interestCategories ?? [],
+ },
+ {
+ title: "관심 기능",
+ items: beauty?.interestFunctions ?? [],
+ },
+ ],
+ };
+ }
+
+ if (trait.id === "fashion") {
+ return {
+ ...trait,
+ previewLines: [
+ { label: "키", value: fashion?.height ?? "" },
+ { label: "체형", value: fashion?.bodyShape ?? "" },
+ { label: "상의", value: fashion?.topSize ?? "" },
+ { label: "하의", value: fashion?.bottomSize ?? "" },
+ ],
+ topSummary: [
+ { label: "키/몸무게", value: fashion?.height ?? "" },
+ { label: "체형", value: fashion?.bodyShape ?? "" },
+ { label: "상의 사이즈", value: fashion?.topSize ?? "" },
+ { label: "하의 사이즈", value: fashion?.bottomSize ?? "" },
+ ],
+ sections: [
+ {
+ title: "관심 분야",
+ items: fashion?.interestFields ?? [],
+ },
+ {
+ title: "관심 스타일",
+ items: fashion?.interestStyles ?? [],
+ },
+ {
+ title: "관심 브랜드",
+ items: fashion?.interestBrands ?? [],
+ },
+ ],
+ };
+ }
+
+ if (trait.id === "content") {
+ return {
+ ...trait,
+ previewLines: [
+ { label: "성별", value: (content?.viewerGender ?? []).join(", ") },
+ { label: "나이대", value: (content?.viewerAge ?? []).join(", ") },
+ { label: "평균 길이", value: content?.avgVideoLength ?? "" },
+ { label: "평균 조회수", value: content?.avgViews ?? "" },
+ ],
+ topSummary: [
+ {
+ label: "주 시청자 성별",
+ value: (content?.viewerGender ?? []).join(", "),
+ },
+ {
+ label: "주 시청자 나이대",
+ value: (content?.viewerAge ?? []).join(", "),
+ },
+ { label: "평균 영상 길이", value: content?.avgVideoLength ?? "" },
+ { label: "평균 조회수", value: content?.avgViews ?? "" },
+ ],
+ sections: [
+ {
+ title: "콘텐츠 형식",
+ items: content?.contentFormats ?? [],
+ },
+ {
+ title: "브랜드 톤",
+ items: content?.contentTones ?? [],
+ },
+ {
+ title: "희망 관여도",
+ items: content?.desiredInvolvement ?? [],
+ },
+ {
+ title: "희망 활용 범위",
+ items: content?.desiredUsageScope ?? [],
+ },
+ ],
+ };
+ }
+
+ return trait;
+ });
+ }, [feature]);
+
+ return (
+
+
+
+ navigate(-1)} />
+
+
+
+
+ {traits.map((trait) => {
+ const cols = trait.topSummary.length;
+ return (
+
+
+
+ {trait.icon("w-[46px] h-[47px]")}
+
+
+ {trait.badge}
+
+
+
+
+
+
+
+
+
+
+
+ {trait.topSummary.map((item, i) => (
+
+
+ {item.label}
+
+
+ {item.value}
+
+
+ ))}
+
+
+
+
+ {trait.sections.map((section, i) => (
+
+
+ {section.title}
+
+
+ {section.items.join(", ")}
+
+
+ ))}
+
+
+
+ );
+ })}
+
+
+
+
+ );
+}
diff --git a/app/routes/rooms/hooks/useAuthScroll.ts b/app/routes/rooms/hooks/useAuthScroll.ts
index 6bb15971..72a9d238 100644
--- a/app/routes/rooms/hooks/useAuthScroll.ts
+++ b/app/routes/rooms/hooks/useAuthScroll.ts
@@ -8,5 +8,5 @@ export function useAutoScroll(
const el = listRef.current;
if (!el) return;
el.scrollTop = el.scrollHeight;
- }, deps);
+ }, [listRef, deps]);
}
diff --git a/app/stores/auth-store.ts b/app/stores/auth-store.ts
index c604e157..8da509a2 100644
--- a/app/stores/auth-store.ts
+++ b/app/stores/auth-store.ts
@@ -1,5 +1,6 @@
// stores/auth-store.ts
import { create } from "zustand";
+import { tokenStorage } from "../lib/token";
export type AuthUser = {
// optional로 둠
@@ -33,6 +34,6 @@ export const useAuthStore = create((set) => ({
logout: () => {
set({ me: null });
- // localStorage.removeItem("accessToken");
+ tokenStorage.clearTokens();
},
}));