Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion app/_components/BubbleDiv.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@ const BubbleDiv = ({
<div>
{children || (
<>
현재 <span className="text-bubble-text-highight">775명</span> 참여중이에요!
현재 <span className="text-bubble-text-highight">775명</span>{" "}
참여중이에요!
</>
)}
</div>
Expand Down
17 changes: 10 additions & 7 deletions app/hobby-select/_components/ScreenHobbySelect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ const ALL_HOBBIES = Object.values(HOBBIES).flat() as string[];
const ScreenHobbySelect = () => {
const router = useRouter();
const { profile, updateProfile } = useProfile();

// 취미 이름으로 카테고리를 찾는 헬퍼 함수
const findCategoryByHobbyName = (name: string): HobbyCategory => {
const found = (Object.keys(HOBBIES) as HobbyCategory[]).find((cat) =>
Expand Down Expand Up @@ -88,10 +88,12 @@ const ScreenHobbySelect = () => {
const getHobbiesByCategory = (category: HobbyCategory) => {
const predefined = HOBBIES[category];
// 사용자가 이 카테고리에 추가했던 모든 커스텀 취미 추출
const customInCategory = addedCustomHobbies.filter(h => h.category === category);
const customInCategory = addedCustomHobbies.filter(
(h) => h.category === category,
);
return {
predefined,
customInCategory
customInCategory,
};
};

Expand All @@ -115,7 +117,7 @@ const ScreenHobbySelect = () => {
<div className="flex flex-wrap gap-3">
{filteredHobbies.length > 0 ? (
filteredHobbies.map((hobby) => {
const isSelected = selected.some(h => h.name === hobby);
const isSelected = selected.some((h) => h.name === hobby);
return (
<HobbyButton
key={hobby}
Expand All @@ -135,7 +137,8 @@ const ScreenHobbySelect = () => {
</div>
) : (
(Object.keys(HOBBIES) as HobbyCategory[]).map((category) => {
const { predefined, customInCategory } = getHobbiesByCategory(category);
const { predefined, customInCategory } =
getHobbiesByCategory(category);
return (
<div key={category} className="flex flex-col">
<h2 className="typo-16-600 mb-3 text-black">{category}</h2>
Expand All @@ -145,7 +148,7 @@ const ScreenHobbySelect = () => {
<HobbyButton
key={hobby}
onClick={() => toggleHobby(hobby)}
selected={selected.some(h => h.name === hobby)}
selected={selected.some((h) => h.name === hobby)}
>
{hobby}
</HobbyButton>
Expand All @@ -155,7 +158,7 @@ const ScreenHobbySelect = () => {
<HobbyButton
key={hobby.name}
onClick={() => toggleHobby(hobby.name, category)}
selected={selected.some(h => h.name === hobby.name)}
selected={selected.some((h) => h.name === hobby.name)}
>
{hobby.name}
</HobbyButton>
Expand Down
28 changes: 15 additions & 13 deletions app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@ import localFont from "next/font/local";
import "./globals.css";
import Blur from "@/components/common/Blur";
import { QueryProvider } from "@/providers/query-provider";
import { ServiceStatusProvider } from "@/providers/service-status-provider";
import { ProfileProvider } from "@/providers/profile-provider";
// import { ServiceStatusProvider } from "@/providers/service-status-provider";
// import { getInitialMaintenanceStatus } from "@/lib/status";
import { getInitialMaintenanceStatus } from "@/lib/status";
import FcmInitializer from "@/components/common/FcmInitializer";

const pretendard = localFont({
src: "./fonts/PretendardVariable.woff2",
Expand Down Expand Up @@ -56,24 +57,25 @@ export default async function RootLayout({
}: Readonly<{
children: React.ReactNode;
}>) {
// const initialMaintenanceMode = await getInitialMaintenanceStatus();
const initialMaintenanceMode = await getInitialMaintenanceStatus();

return (
<html lang="ko" className={pretendard.variable}>
<body
className={`${pretendard.className} flex justify-center bg-white antialiased`}
>
<QueryProvider>
<ProfileProvider>
{/* <ServiceStatusProvider */}
{/* initialMaintenanceMode={initialMaintenanceMode} */}
{/* > */}
<div className="bg-background-app-base relative isolate min-h-dvh w-full overflow-x-hidden text-black md:max-w-[430px] md:shadow-lg">
<Blur />
{children}
</div>
{/* </ServiceStatusProvider> */}
</ProfileProvider>
<ServiceStatusProvider
initialMaintenanceMode={initialMaintenanceMode}
>
<ProfileProvider>
<div className="bg-background-app-base relative min-h-dvh w-full overflow-x-hidden text-black md:max-w-[430px] md:shadow-lg">
<Blur />
<FcmInitializer />
{children}
</div>
</ProfileProvider>
</ServiceStatusProvider>
</QueryProvider>
</body>
</html>
Expand Down
4 changes: 1 addition & 3 deletions app/login/_components/LoginForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -68,9 +68,7 @@ export const LoginForm = () => {
</div>

<div className="mt-auto flex w-full flex-col items-center gap-4">
<BubbleDiv top={-4}>
아직 계정이 없으신가요?!
</BubbleDiv>
<BubbleDiv top={-4}>아직 계정이 없으신가요?!</BubbleDiv>
<Link
href="/register"
className="typo-14-500 flex items-center gap-1 border-b-2 border-gray-500 text-gray-500"
Expand Down
45 changes: 45 additions & 0 deletions components/common/FcmInitializer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
"use client";

import { useEffect } from "react";
import { registerServiceWorkerAndGetToken } from "@/lib/firebase";
import { api } from "@/lib/axios";

const FCM_REGISTERED_KEY = "fcm_registered";

export default function FcmInitializer() {
useEffect(() => {
// 세션 당 1회만 실행 (페이지 이동마다 중복 등록 방지)
if (sessionStorage.getItem(FCM_REGISTERED_KEY)) return;

const registerFcmToken = async () => {
try {
// 알림 권한 요청
const permission = await Notification.requestPermission();
if (permission !== "granted") {
console.log("[FCM] 알림 권한 거부됨. 토큰 등록 skip.");
return;
}

// Firebase에서 FCM 토큰 발급
const token = await registerServiceWorkerAndGetToken();
if (!token) {
console.warn("[FCM] 토큰 발급 실패.");
return;
}

// 백엔드에 FCM 토큰 등록
await api.post("/api/fcm/token", { token });

// 세션 플래그 저장 (재등록 방지)
sessionStorage.setItem(FCM_REGISTERED_KEY, "true");
console.log("[FCM] 토큰 등록 완료.");
} catch (error) {
console.error("[FCM] 토큰 등록 중 오류:", error);
}
};

registerFcmToken();
}, []);
Comment on lines +10 to +42
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

FcmInitializer 컴포넌트는 RootLayout에 포함되어 모든 페이지에서 렌더링됩니다. 현재 구현은 사용자의 인증 상태와 관계없이 FCM 토큰 등록을 시도합니다.

문제점:

  • 미인증 사용자: 로그인하지 않은 사용자의 경우, 발급된 토큰을 특정 사용자와 연결할 수 없어 무의미한 토큰이 됩니다.
  • 불필요한 API 호출: 미인증 상태에서도 백엔드에 토큰 등록 API(api.post("/api/fcm/token", ...) )를 호출하게 되어 불필요한 네트워크 요청이 발생합니다.

개선 제안:
토큰 등록 로직은 사용자가 로그인한 상태일 때만 실행되도록 변경해야 합니다. useProfile 훅 등을 사용하여 사용자 인증 상태를 확인하고, 인증된 경우에만 registerFcmToken 함수를 호출하도록 수정하는 것을 권장합니다.


return null;
}
1 change: 1 addition & 0 deletions lib/axios.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ api.interceptors.response.use(
return api(originalRequest);
} catch (reissueError) {
// 재발급 실패 → 로그인 페이지로 리다이렉트
alert("로그인 세션이 만료되었습니다. 다시 로그인해주세요.");
window.location.href = "/login";
return Promise.reject(reissueError);
}
Expand Down
103 changes: 94 additions & 9 deletions lib/constants/nicknames.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,108 @@
export const NICKNAME_MODIFIERS = [
// 리듬/움직임
"노래하는", "춤추는", "뛰어노는", "구르는", "빙글도는", "날아가는", "흐느적이는", "뒹구는", "달리는", "흔들리는",
"노래하는",
"춤추는",
"뛰어노는",
"구르는",
"빙글도는",
"날아가는",
"흐느적이는",
"뒹구는",
"달리는",
"흔들리는",
// 감정/표정
"웃는", "반짝이는", "두근대는", "깔깔대는", "놀라는", "감탄하는", "감동한", "행복한", "수줍은", "즐거운",
"웃는",
"반짝이는",
"두근대는",
"깔깔대는",
"놀라는",
"감탄하는",
"감동한",
"행복한",
"수줍은",
"즐거운",
// 상태/분위기
"반짝반짝한", "포근한", "시원한", "따뜻한", "향기나는", "알록달록한", "몽글몽글한", "부드러운", "촉촉한",
"반짝반짝한",
"포근한",
"시원한",
"따뜻한",
"향기나는",
"알록달록한",
"몽글몽글한",
"부드러운",
"촉촉한",
// 행동 스타일
"장난치는", "휘파람 부는", "손흔드는", "인사하는", "하품하는", "손뻗는", "끄덕이는", "기지개 켜는", "숨바꼭질하는",
"장난치는",
"휘파람 부는",
"손흔드는",
"인사하는",
"하품하는",
"손뻗는",
"끄덕이는",
"기지개 켜는",
"숨바꼭질하는",
// 캐릭터 감성
"멋부린", "잠에서 깬", "바람 타는", "구경하는", "편지 쓰는", "노을 보는", "초콜릿 든", "선물 고르는"
"멋부린",
"잠에서 깬",
"바람 타는",
"구경하는",
"편지 쓰는",
"노을 보는",
"초콜릿 든",
"선물 고르는",
];

export const NICKNAME_NOUNS = [
// 동물
"나무늘보", "햄스터", "다람쥐", "고슴도치", "기린", "오리", "판다", "앵무새", "물개", "코알라", "돌고래", "코끼리", "라마", "고래", "아기곰",
"나무늘보",
"햄스터",
"다람쥐",
"고슴도치",
"기린",
"오리",
"판다",
"앵무새",
"물개",
"코알라",
"돌고래",
"코끼리",
"라마",
"고래",
"아기곰",
// 식물/꽃
"데이지", "민들레", "코스모스", "라벤더", "동백꽃", "연꽃", "수국", "벚꽃잎", "클로버", "벚꽃", "해바라기",
"데이지",
"민들레",
"코스모스",
"라벤더",
"동백꽃",
"연꽃",
"수국",
"벚꽃잎",
"클로버",
"벚꽃",
"해바라기",
// 자연/사물
"파도", "구름", "별똥별", "바람", "노을", "햇살", "모래성", "무지개", "달빛", "눈송이",
"파도",
"구름",
"별똥별",
"바람",
"노을",
"햇살",
"모래성",
"무지개",
"달빛",
"눈송이",
// 캐릭터형 단어
"화가", "작가", "바리스타", "제빵사", "소방관", "탐험가", "마술사", "사진작가", "연기자", "시인", "조향사", "고고학자"
"화가",
"작가",
"바리스타",
"제빵사",
"소방관",
"탐험가",
"마술사",
"사진작가",
"연기자",
"시인",
"조향사",
"고고학자",
];
69 changes: 69 additions & 0 deletions lib/firebase.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
"use client";

import { initializeApp, getApps } from "firebase/app";
import { getAnalytics } from "firebase/analytics";
import { getMessaging, getToken, onMessage } from "firebase/messaging";

export const firebaseConfig = {
apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY,
authDomain: process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN,
projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID,
storageBucket: process.env.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET,
messagingSenderId: process.env.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID,
appId: process.env.NEXT_PUBLIC_FIREBASE_APP_ID,
measurementId: process.env.NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID,
};

// Initialize Firebase (중복 초기화 방지)
const app =
getApps().length === 0 ? initializeApp(firebaseConfig) : getApps()[0];

// Analytics (브라우저에서만 실행)
let analytics;
if (typeof window !== "undefined") {
analytics = getAnalytics(app);
}

// 서비스 워커 등록 및 FCM 토큰 가져오기
export async function registerServiceWorkerAndGetToken() {
if (typeof window === "undefined" || !("serviceWorker" in navigator)) {
return null;
}

try {
// 서비스 워커 등록
const registration = await navigator.serviceWorker.register(
"/firebase-messaging-sw.js",
);

// 서비스 워커에 Firebase Config 전달
registration.active?.postMessage({
type: "INIT_FIREBASE",
config: firebaseConfig,
});

// Messaging 인스턴스 가져오기
const messaging = getMessaging(app);

// FCM 토큰 요청
const token = await getToken(messaging, {
vapidKey: process.env.NEXT_PUBLIC_FIREBASE_VAPID_KEY,
serviceWorkerRegistration: registration,
});

console.log("FCM Token:", token);

// 포그라운드 메시지 수신 리스너
onMessage(messaging, (payload) => {
console.log("[Foreground] 메시지 수신:", payload);
// 여기에 포그라운드 알림 UI 처리
});

return token;
} catch (error) {
console.error("서비스 워커 등록 실패:", error);
return null;
}
}

export { app, analytics };
1 change: 1 addition & 0 deletions lib/server-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ serverClient.interceptors.response.use(
return serverClient(originalRequest);
} catch {
// 재발급 실패 → 로그인 페이지로
alert("로그인 세션이 만료되었습니다. 다시 로그인해주세요.");
redirect("/login");
}
}
Expand Down
6 changes: 4 additions & 2 deletions lib/utils/nickname.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ import { NICKNAME_MODIFIERS, NICKNAME_NOUNS } from "../constants/nicknames";
* 랜덤한 닉네임을 생성합니다 (수식어 + 명사)
*/
export const generateRandomNickname = () => {
const modifier = NICKNAME_MODIFIERS[Math.floor(Math.random() * NICKNAME_MODIFIERS.length)];
const noun = NICKNAME_NOUNS[Math.floor(Math.random() * NICKNAME_NOUNS.length)];
const modifier =
NICKNAME_MODIFIERS[Math.floor(Math.random() * NICKNAME_MODIFIERS.length)];
const noun =
NICKNAME_NOUNS[Math.floor(Math.random() * NICKNAME_NOUNS.length)];
return `${modifier} ${noun}`;
};
Loading
Loading