From feed282bedce2a9efa59ffe4b586260a5fd86db8 Mon Sep 17 00:00:00 2001 From: dasosann Date: Mon, 18 May 2026 10:14:05 +0900 Subject: [PATCH 1/7] =?UTF-8?q?feat:=20=ED=94=84=EB=A1=9C=ED=95=84=20?= =?UTF-8?q?=EC=9D=B4=EB=AF=B8=EC=A7=80=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/mypage/_components/ScreenMyPage.tsx | 26 ++++++++++++++++++++++--- lib/constants/defaultProfiles.ts | 8 ++++++++ lib/utils/profile.ts | 1 + next.config.ts | 4 ++++ 4 files changed, 36 insertions(+), 3 deletions(-) diff --git a/app/mypage/_components/ScreenMyPage.tsx b/app/mypage/_components/ScreenMyPage.tsx index f204b2a..6b5b47e 100644 --- a/app/mypage/_components/ScreenMyPage.tsx +++ b/app/mypage/_components/ScreenMyPage.tsx @@ -11,6 +11,7 @@ import { cn } from "@/lib/utils"; import { getDefaultProfilesByGender, DEFAULT_PROFILE_ASSETS, + getAutoSwitchProfileIdByGender, } from "@/lib/constants/defaultProfiles"; import type { Gender, @@ -473,6 +474,25 @@ const ScreenMyPage = ({ initialProfile }: ScreenMyPageProps) => { }; /* ───── 프로필 이미지 핸들러 ───── */ + const handleGenderChange = (newGenderKo: "남자" | "여자") => { + setGender(newGenderKo); + const newGenderKey = genderMap[newGenderKo]; + + // 성별 변경 시 현재 선택된 동물 캐릭터의 대응되는 성별 이미지가 있는지 확인 + const nextProfileId = getAutoSwitchProfileIdByGender( + profileImageUrl, + newGenderKey, + ); + if (nextProfileId) { + setProfileImageUrl(nextProfileId); + } else { + const availableProfiles = getDefaultProfilesByGender(newGenderKey); + if (availableProfiles.length > 0) { + setProfileImageUrl(availableProfiles[0].id); + } + } + }; + const handleSelectProfileType = (type: "default" | "custom") => { setSelectedType(type); if (type === "default") { @@ -556,7 +576,7 @@ const ScreenMyPage = ({ initialProfile }: ScreenMyPageProps) => { { setProfileImageUrl(id); @@ -737,13 +757,13 @@ const ScreenMyPage = ({ initialProfile }: ScreenMyPageProps) => {
setGender("여자")} + onClick={() => handleGenderChange("여자")} > 여자 setGender("남자")} + onClick={() => handleGenderChange("남자")} > 남자 diff --git a/lib/constants/defaultProfiles.ts b/lib/constants/defaultProfiles.ts index 1d9b438..faa9e16 100644 --- a/lib/constants/defaultProfiles.ts +++ b/lib/constants/defaultProfiles.ts @@ -120,6 +120,14 @@ export function getAutoSwitchProfileIdByGender( return null; } + // 뱀(여성전용) ↔ 말(남성전용) 특수 스위칭 매핑 + if (currentProfileId === "snake" && newGender === "MALE") { + return "horse"; + } + if (currentProfileId === "horse" && newGender === "FEMALE") { + return "snake"; + } + const asset = DEFAULT_PROFILE_ASSETS.find((p) => p.id === currentProfileId); if (!asset) { return null; diff --git a/lib/utils/profile.ts b/lib/utils/profile.ts index bc11b93..0bbd373 100644 --- a/lib/utils/profile.ts +++ b/lib/utils/profile.ts @@ -36,6 +36,7 @@ export const getProfileImageUrl = ( const animalId = filename .replace("animal_", "") .replace("default_", "") + .replace(/_(male|female)/i, "") // _male 또는 _female 접미사 제거 .replace(/\d+.*$/, "") // animal_dinosaur1.png -> dinosaur .replace(/\..*$/, ""); // 확장자 제거 diff --git a/next.config.ts b/next.config.ts index ae1e456..c1b5bd0 100644 --- a/next.config.ts +++ b/next.config.ts @@ -32,6 +32,10 @@ const nextConfig: NextConfig = { protocol: "https", hostname: "comatching.site", }, + { + protocol: "https", + hostname: "srv.comatching.site", + }, { protocol: "https", hostname: "comatching5.s3.ap-northeast-2.amazonaws.com", From 257e1e7feb8d29874379116a80653adfd0e98ebf Mon Sep 17 00:00:00 2001 From: dasosann Date: Mon, 18 May 2026 10:42:41 +0900 Subject: [PATCH 2/7] =?UTF-8?q?feat:=20QA=20=EC=82=AC=ED=95=AD=20=EB=B0=98?= =?UTF-8?q?=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../_components/YesMatchingList.tsx | 4 ++- .../_components/ScreenMatchingResult.tsx | 2 +- .../_components/ImportantOptionDrawer.tsx | 27 +++++++++++++++++-- .../_components/MatchingAgeOption.tsx | 22 ++++++++++----- app/matching/_components/ScreenMatching.tsx | 16 ++++++----- hooks/useChatMemberProfile.ts | 4 +-- lib/types/matching.ts | 2 +- 7 files changed, 57 insertions(+), 20 deletions(-) diff --git a/app/matching-list/_components/YesMatchingList.tsx b/app/matching-list/_components/YesMatchingList.tsx index 5547951..077516b 100644 --- a/app/matching-list/_components/YesMatchingList.tsx +++ b/app/matching-list/_components/YesMatchingList.tsx @@ -44,13 +44,15 @@ const YesMatchingList = ({ // 검색 필터 if (searchQuery.trim()) { const query = searchQuery.trim().toLowerCase(); + const cleanQuery = query.replace("살", "").trim(); result = result.filter((item) => { const p = item.partner; + const rawAge = p.age || (p.birthDate ? getAge(p.birthDate) : null); return ( p.nickname.toLowerCase().includes(query) || p.mbti.toLowerCase().includes(query) || p.major.toLowerCase().includes(query) || - (p.birthDate && String(getAge(p.birthDate)).includes(query)) + (rawAge !== null && String(rawAge).includes(cleanQuery)) ); }); } diff --git a/app/matching-result/_components/ScreenMatchingResult.tsx b/app/matching-result/_components/ScreenMatchingResult.tsx index 2ae2c3d..8ba93f4 100644 --- a/app/matching-result/_components/ScreenMatchingResult.tsx +++ b/app/matching-result/_components/ScreenMatchingResult.tsx @@ -57,7 +57,7 @@ const ScreenMatchingResult = () => { return (
router.push("/main")} + onClick={() => router.push("/matching")} text={
{displayedText}; } +const formatMBTISelection = (mbti: string): string => { + const mbtiMap: Record = { + E: "외향형", + I: "내향형", + S: "감각형", + N: "직관형", + T: "사고형", + F: "감정형", + J: "판단형", + P: "인식형", + }; + const chars = mbti.split("").filter(Boolean); + const labels = chars.map((c) => mbtiMap[c] || c); + if (labels.length === 2) { + return `${labels[0]}, 그리고 ${labels[1]}`; + } + return labels.join(", "); +}; + export default function ImportantOptionDrawer({ trigger, onSelect, @@ -224,7 +243,7 @@ export default function ImportantOptionDrawer({
) : ( -
+
{typingStep >= 1 && (
@@ -241,7 +260,11 @@ export default function ImportantOptionDrawer({
diff --git a/app/matching/_components/MatchingAgeOption.tsx b/app/matching/_components/MatchingAgeOption.tsx index 85d6ed9..bdf5bb0 100644 --- a/app/matching/_components/MatchingAgeOption.tsx +++ b/app/matching/_components/MatchingAgeOption.tsx @@ -61,9 +61,14 @@ export default function MatchingAgeOption({ onConfirm={handleConfirm} trigger={ +
+
+ ); +} diff --git a/lib/firebase.ts b/lib/firebase.ts index 1754ee9..2956c5e 100644 --- a/lib/firebase.ts +++ b/lib/firebase.ts @@ -3,6 +3,7 @@ import { initializeApp, getApps } from "firebase/app"; import { getAnalytics } from "firebase/analytics"; import { getMessaging, getToken, onMessage } from "firebase/messaging"; +import { useToastStore } from "@/stores/toast-store"; export const firebaseConfig = { apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY, @@ -74,8 +75,24 @@ export async function registerServiceWorkerAndGetToken() { // 포그라운드 메시지 수신 리스너 onMessage(messaging, (payload) => { - console.log("[Foreground] 메시지 수신:", payload); - // 여기에 포그라운드 알림 UI 처리 + console.log("🔔 [FCM Foreground Payload] 수신된 알림 전체 구조:"); + console.log(JSON.stringify(payload, null, 2)); + console.dir(payload); + + // notification 이나 data 필드에서 어떻게든 정보를 추출합니다. + const title = + payload.notification?.title || payload.data?.title || "새로운 알림"; + const body = + payload.notification?.body || + payload.data?.body || + payload.data?.message || + ""; + + // 커스텀 인앱 토스트 알림 노출 + useToastStore.getState().showToast({ + title, + body, + }); }); return token; diff --git a/stores/toast-store.ts b/stores/toast-store.ts new file mode 100644 index 0000000..39163cd --- /dev/null +++ b/stores/toast-store.ts @@ -0,0 +1,33 @@ +import { create } from "zustand"; + +interface Toast { + id: number; + title: string; + body: string; + icon?: string; +} + +interface ToastState { + toast: Toast | null; + showToast: (params: { title: string; body: string; icon?: string }) => void; + hideToast: () => void; +} + +export const useToastStore = create((set) => ({ + toast: null, + showToast: (params) => { + const id = Date.now(); + set({ toast: { id, ...params } }); + + // 4초 후 자동으로 토스트 닫기 + setTimeout(() => { + set((state) => { + if (state.toast?.id === id) { + return { toast: null }; + } + return state; + }); + }, 4000); + }, + hideToast: () => set({ toast: null }), +})); From 949fb4d9178ae990798ddec8025321ce55150a5d Mon Sep 17 00:00:00 2001 From: dasosann Date: Mon, 18 May 2026 14:35:28 +0900 Subject: [PATCH 6/7] =?UTF-8?q?feat:=20QA=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/login/_components/LoginForm.tsx | 7 ++- app/mypage/_components/ScreenMyPage.tsx | 44 ++++--------------- app/profile-image/_components/TermsDrawer.tsx | 6 ++- app/register/_components/ScreenRegister.tsx | 3 +- app/register/_components/VerificationStep.tsx | 2 +- .../new/_components/ScreenNewPasswordPage.tsx | 4 ++ 6 files changed, 26 insertions(+), 40 deletions(-) diff --git a/app/login/_components/LoginForm.tsx b/app/login/_components/LoginForm.tsx index d295078..4f729b2 100644 --- a/app/login/_components/LoginForm.tsx +++ b/app/login/_components/LoginForm.tsx @@ -58,9 +58,12 @@ export const LoginForm = () => {
- + alert("미지원 서비스입니다.")} + className="cursor-pointer" + > 이메일 찾기 - + | 비밀번호 변경 diff --git a/app/mypage/_components/ScreenMyPage.tsx b/app/mypage/_components/ScreenMyPage.tsx index e6204e4..8390497 100644 --- a/app/mypage/_components/ScreenMyPage.tsx +++ b/app/mypage/_components/ScreenMyPage.tsx @@ -492,26 +492,6 @@ const ScreenMyPage = ({ initialProfile }: ScreenMyPageProps) => { } }; - /* ───── 프로필 이미지 핸들러 ───── */ - const handleGenderChange = (newGenderKo: "남자" | "여자") => { - setGender(newGenderKo); - const newGenderKey = genderMap[newGenderKo]; - - // 성별 변경 시 현재 선택된 동물 캐릭터의 대응되는 성별 이미지가 있는지 확인 - const nextProfileId = getAutoSwitchProfileIdByGender( - profileImageUrl, - newGenderKey, - ); - if (nextProfileId) { - setProfileImageUrl(nextProfileId); - } else { - const availableProfiles = getDefaultProfilesByGender(newGenderKey); - if (availableProfiles.length > 0) { - setProfileImageUrl(availableProfiles[0].id); - } - } - }; - const handleSelectProfileType = (type: "default" | "custom") => { setSelectedType(type); if (type === "default") { @@ -527,9 +507,13 @@ const ScreenMyPage = ({ initialProfile }: ScreenMyPageProps) => { const currentHobbies = JSON.stringify(hobbies); const currentSocialId = socialType === "INSTAGRAM" - ? socialAccountId + ? socialAccountId.trim() + ? socialAccountId.trim().startsWith("@") + ? socialAccountId.trim() + : `@${socialAccountId.trim()}` + : "" : socialType === "KAKAO" - ? kakaoId + ? kakaoId.trim() : ""; return checkProfileChanged({ @@ -773,19 +757,9 @@ const ScreenMyPage = ({ initialProfile }: ScreenMyPageProps) => { {/* ── 성별 ── */}
-
- handleGenderChange("여자")} - > - 여자 - - handleGenderChange("남자")} - > - 남자 - +
+ 여자 + 남자
diff --git a/app/profile-image/_components/TermsDrawer.tsx b/app/profile-image/_components/TermsDrawer.tsx index 9fb6716..9b69044 100644 --- a/app/profile-image/_components/TermsDrawer.tsx +++ b/app/profile-image/_components/TermsDrawer.tsx @@ -148,8 +148,12 @@ const TermsDrawer = ({ children }: TermsDrawerProps) => { if (result.success) { setIsOpen(false); - clearProfile(); + // 페이지 이동이 완료되기 전에 프로필 데이터가 비워져서 '여성 강아지(female_dog)' 기본 이미지로 화면이 깜빡이는 현상을 방지하기 위해, + // 먼저 이동하고 약간의 딜레이(500ms) 후에 프로필 저장소를 클리어합니다. router.push("/main"); + setTimeout(() => { + clearProfile(); + }, 500); } else { alert(result.message); } diff --git a/app/register/_components/ScreenRegister.tsx b/app/register/_components/ScreenRegister.tsx index 28dea8f..b0a01d2 100644 --- a/app/register/_components/ScreenRegister.tsx +++ b/app/register/_components/ScreenRegister.tsx @@ -55,7 +55,8 @@ export const ScreenRegister = () => { { onSuccess: (data) => { if (data.status === 200) { - // TODO: 회원가입 완료 후 이동 (예: router.push("/login")) + alert("가입이 완료되었습니다."); + router.push("/login"); } else { alert("회원가입에 실패했습니다. 다시 시도해주세요."); } diff --git a/app/register/_components/VerificationStep.tsx b/app/register/_components/VerificationStep.tsx index c74b059..60302f3 100644 --- a/app/register/_components/VerificationStep.tsx +++ b/app/register/_components/VerificationStep.tsx @@ -143,7 +143,7 @@ export const VerificationStep = ({
-
+
인증번호가 오지 않았나요?
이전 단계에서 이메일을 재확인하거나, 스팸함을 확인해 diff --git a/app/reset/password/new/_components/ScreenNewPasswordPage.tsx b/app/reset/password/new/_components/ScreenNewPasswordPage.tsx index 4c88021..71c5a3c 100644 --- a/app/reset/password/new/_components/ScreenNewPasswordPage.tsx +++ b/app/reset/password/new/_components/ScreenNewPasswordPage.tsx @@ -74,6 +74,10 @@ const ScreenNewPasswordPage = () => { }, onError: (error) => { + // '존재하지 않는 유저' 에러가 이미 표시된 상태라면, 중복 클릭 시 다른 에러(인증번호 오류 등)로 덮어쓰지 않고 유지합니다. + if (errorMessage.includes("존재하지 않는")) { + return; + } setErrorMessage( error.message || "비밀번호 변경에 실패했습니다. 다시 시도해 주세요.", From 8178e687dcd2a898d35d9d8ac2f39349bd29c949 Mon Sep 17 00:00:00 2001 From: dasosann Date: Mon, 18 May 2026 14:44:29 +0900 Subject: [PATCH 7/7] =?UTF-8?q?feat:=20disabled=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/mypage/_components/ScreenMyPage.tsx | 10 +++++++--- app/profile-builder/_components/ProfileButton.tsx | 4 ++++ 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/app/mypage/_components/ScreenMyPage.tsx b/app/mypage/_components/ScreenMyPage.tsx index 8390497..4afc130 100644 --- a/app/mypage/_components/ScreenMyPage.tsx +++ b/app/mypage/_components/ScreenMyPage.tsx @@ -757,9 +757,13 @@ const ScreenMyPage = ({ initialProfile }: ScreenMyPageProps) => { {/* ── 성별 ── */}
-
- 여자 - 남자 +
+ + 여자 + + + 남자 +
diff --git a/app/profile-builder/_components/ProfileButton.tsx b/app/profile-builder/_components/ProfileButton.tsx index 2c6b8a6..4408263 100644 --- a/app/profile-builder/_components/ProfileButton.tsx +++ b/app/profile-builder/_components/ProfileButton.tsx @@ -7,12 +7,14 @@ interface ProfileButtonProps { children: React.ReactNode; selected?: boolean; onClick?: () => void; + disabled?: boolean; } export default function ProfileButton({ children, selected = false, onClick, + disabled = false, }: ProfileButtonProps) { return (