diff --git a/backend/src/services/member.js b/backend/src/services/member.js index f01b15c..c55c6c9 100644 --- a/backend/src/services/member.js +++ b/backend/src/services/member.js @@ -62,8 +62,9 @@ export async function invalidateMemberCache(redis) { /** * 이름으로 멤버 조회 (별명 포함) + * 한글명(name) 또는 영문명(name_en) 모두 검색 가능 * @param {object} db - 데이터베이스 연결 - * @param {string} name - 멤버 이름 + * @param {string} name - 멤버 이름 (한글 또는 영문) * @returns {object|null} 멤버 정보 또는 null */ export async function getMemberByName(db, name) { @@ -75,8 +76,8 @@ export async function getMemberByName(db, name) { i.thumb_url as image_thumb FROM members m LEFT JOIN images i ON m.image_id = i.id - WHERE m.name = ? - `, [name]); + WHERE m.name = ? OR LOWER(m.name_en) = LOWER(?) + `, [name, name]); if (members.length === 0) { return null; diff --git a/frontend/src/pages/mobile/schedule/Birthday.jsx b/frontend/src/pages/mobile/schedule/Birthday.jsx index ce3c4b1..8aab694 100644 --- a/frontend/src/pages/mobile/schedule/Birthday.jsx +++ b/frontend/src/pages/mobile/schedule/Birthday.jsx @@ -1,4 +1,4 @@ -import { useParams, Link } from 'react-router-dom'; +import { Link } from 'react-router-dom'; import { useQuery } from '@tanstack/react-query'; import { motion } from 'framer-motion'; import { ChevronLeft } from 'lucide-react'; @@ -6,25 +6,23 @@ import { fetchApi } from '@/api'; /** * Mobile 생일 페이지 + * @param {object} props + * @param {string} props.year - 연도 + * @param {string} props.nameEn - 멤버 영문 이름 (소문자) */ -function MobileBirthday() { - const { memberName, year } = useParams(); - - // URL 디코딩 - const decodedMemberName = decodeURIComponent(memberName || ''); - - // 멤버 정보 조회 +function MobileBirthday({ year, nameEn }) { + // 멤버 정보 조회 (영문 이름으로) const { data: member, isLoading: memberLoading, error, } = useQuery({ - queryKey: ['member', decodedMemberName], - queryFn: () => fetchApi(`/members/${encodeURIComponent(decodedMemberName)}`), - enabled: !!decodedMemberName, + queryKey: ['member', nameEn], + queryFn: () => fetchApi(`/members/${encodeURIComponent(nameEn)}`), + enabled: !!nameEn, }); - if (!decodedMemberName || error) { + if (!nameEn || error) { return (
@@ -128,7 +126,7 @@ function MobileBirthday() {
🎁

- {year}년 {decodedMemberName} 생일카페 정보가 준비 중입니다 + {year}년 {member?.name} 생일카페 정보가 준비 중입니다

생일카페 정보가 등록되면 이곳에 표시됩니다

diff --git a/frontend/src/pages/mobile/schedule/Schedule.jsx b/frontend/src/pages/mobile/schedule/Schedule.jsx index 69c0432..a602287 100644 --- a/frontend/src/pages/mobile/schedule/Schedule.jsx +++ b/frontend/src/pages/mobile/schedule/Schedule.jsx @@ -777,11 +777,7 @@ function MobileSchedule() { key={schedule.id} schedule={schedule} delay={index * 0.05} - onClick={() => { - const scheduleYear = new Date(schedule.date).getFullYear(); - const memberName = schedule.member_names; - navigate(`/birthday/${encodeURIComponent(memberName)}/${scheduleYear}`); - }} + onClick={() => navigate(`/schedule/${schedule.id}`)} /> ); } diff --git a/frontend/src/pages/mobile/schedule/ScheduleDetail.jsx b/frontend/src/pages/mobile/schedule/ScheduleDetail.jsx index cef689c..c19cb00 100644 --- a/frontend/src/pages/mobile/schedule/ScheduleDetail.jsx +++ b/frontend/src/pages/mobile/schedule/ScheduleDetail.jsx @@ -6,6 +6,34 @@ import { Calendar, Clock, ChevronLeft, Link2, X, ChevronRight } from 'lucide-rea import Linkify from 'react-linkify'; import { getSchedule } from '@/api'; import { decodeHtmlEntities, formatFullDate, formatTime, formatXDateTimeWithTime } from '@/utils'; +import Birthday from './Birthday'; + +/** + * 특수 일정 ID 파싱 + * @param {string} id - 일정 ID + * @returns {object|null} { type, year, nameEn } 또는 null + */ +function parseSpecialId(id) { + // birthday-{year}-{nameEn} 형식 + const birthdayMatch = id.match(/^birthday-(\d{4})-(.+)$/); + if (birthdayMatch) { + return { type: 'birthday', year: birthdayMatch[1], nameEn: birthdayMatch[2] }; + } + + // debut-{year} 형식 + const debutMatch = id.match(/^debut-(\d{4})$/); + if (debutMatch) { + return { type: 'debut', year: debutMatch[1] }; + } + + // anniversary-{year} 형식 + const anniversaryMatch = id.match(/^anniversary-(\d{4})$/); + if (anniversaryMatch) { + return { type: 'anniversary', year: anniversaryMatch[1] }; + } + + return null; +} /** * 전체화면 시 자동 가로 회전 훅 (숏츠가 아닐 때만) @@ -393,6 +421,12 @@ function MobileDefaultSection({ schedule }) { function MobileScheduleDetail() { const { id } = useParams(); + // 특수 일정 ID 체크 + const specialId = parseSpecialId(id); + if (specialId?.type === 'birthday') { + return ; + } + // 모바일 레이아웃 활성화 useEffect(() => { document.documentElement.classList.add('mobile-layout'); diff --git a/frontend/src/pages/pc/public/schedule/Birthday.jsx b/frontend/src/pages/pc/public/schedule/Birthday.jsx index 620c7c3..c44a6ff 100644 --- a/frontend/src/pages/pc/public/schedule/Birthday.jsx +++ b/frontend/src/pages/pc/public/schedule/Birthday.jsx @@ -1,4 +1,4 @@ -import { useParams, Link } from 'react-router-dom'; +import { Link } from 'react-router-dom'; import { useQuery } from '@tanstack/react-query'; import { motion } from 'framer-motion'; import { ChevronRight } from 'lucide-react'; @@ -6,25 +6,23 @@ import { fetchApi } from '@/api'; /** * PC 생일 페이지 + * @param {object} props + * @param {string} props.year - 연도 + * @param {string} props.nameEn - 멤버 영문 이름 (소문자) */ -function PCBirthday() { - const { memberName, year } = useParams(); - - // URL 디코딩 - const decodedMemberName = decodeURIComponent(memberName || ''); - - // 멤버 정보 조회 +function PCBirthday({ year, nameEn }) { + // 멤버 정보 조회 (영문 이름으로) const { data: member, isLoading: memberLoading, error, } = useQuery({ - queryKey: ['member', decodedMemberName], - queryFn: () => fetchApi(`/members/${encodeURIComponent(decodedMemberName)}`), - enabled: !!decodedMemberName, + queryKey: ['member', nameEn], + queryFn: () => fetchApi(`/members/${encodeURIComponent(nameEn)}`), + enabled: !!nameEn, }); - if (!decodedMemberName || error) { + if (!nameEn || error) { return (
@@ -125,7 +123,7 @@ function PCBirthday() {
🎁

- {year}년 {decodedMemberName} 생일카페 정보가 준비 중입니다 + {year}년 {member?.name} 생일카페 정보가 준비 중입니다

생일카페 정보가 등록되면 이곳에 표시됩니다

diff --git a/frontend/src/pages/pc/public/schedule/Schedule.jsx b/frontend/src/pages/pc/public/schedule/Schedule.jsx index ab0276e..ae9a58d 100644 --- a/frontend/src/pages/pc/public/schedule/Schedule.jsx +++ b/frontend/src/pages/pc/public/schedule/Schedule.jsx @@ -262,15 +262,17 @@ function PCSchedule() { // 일정 클릭 핸들러 const handleScheduleClick = (schedule) => { - if (schedule.is_birthday || String(schedule.id).startsWith('birthday-')) { - const scheduleYear = new Date(schedule.date).getFullYear(); - navigate(`/birthday/${encodeURIComponent(schedule.member_names)}/${scheduleYear}`); + // 생일, 데뷔, 주년 등 특수 일정 + if (schedule.is_birthday || schedule.is_debut || schedule.is_anniversary) { + navigate(`/schedule/${schedule.id}`); return; } + // 유튜브(2), X(3), 콘서트(6) 카테고리 if ([2, 3, 6].includes(schedule.category_id)) { navigate(`/schedule/${schedule.id}`); return; } + // 소스 URL이 있으면 외부 링크로 if (!schedule.description && schedule.source?.url) { window.open(schedule.source.url, '_blank'); } else { diff --git a/frontend/src/pages/pc/public/schedule/ScheduleDetail.jsx b/frontend/src/pages/pc/public/schedule/ScheduleDetail.jsx index 44848a4..c664ba3 100644 --- a/frontend/src/pages/pc/public/schedule/ScheduleDetail.jsx +++ b/frontend/src/pages/pc/public/schedule/ScheduleDetail.jsx @@ -6,6 +6,34 @@ import { getSchedule } from '@/api'; // 섹션 컴포넌트들 import { YoutubeSection, XSection, DefaultSection, decodeHtmlEntities } from './sections'; +import Birthday from './Birthday'; + +/** + * 특수 일정 ID 파싱 + * @param {string} id - 일정 ID + * @returns {object|null} { type, year, nameEn } 또는 null + */ +function parseSpecialId(id) { + // birthday-{year}-{nameEn} 형식 + const birthdayMatch = id.match(/^birthday-(\d{4})-(.+)$/); + if (birthdayMatch) { + return { type: 'birthday', year: birthdayMatch[1], nameEn: birthdayMatch[2] }; + } + + // debut-{year} 형식 + const debutMatch = id.match(/^debut-(\d{4})$/); + if (debutMatch) { + return { type: 'debut', year: debutMatch[1] }; + } + + // anniversary-{year} 형식 + const anniversaryMatch = id.match(/^anniversary-(\d{4})$/); + if (anniversaryMatch) { + return { type: 'anniversary', year: anniversaryMatch[1] }; + } + + return null; +} /** * PC 일정 상세 페이지 @@ -13,6 +41,12 @@ import { YoutubeSection, XSection, DefaultSection, decodeHtmlEntities } from './ function PCScheduleDetail() { const { id } = useParams(); + // 특수 일정 ID 체크 + const specialId = parseSpecialId(id); + if (specialId?.type === 'birthday') { + return ; + } + const { data: schedule, isLoading, diff --git a/frontend/src/routes/mobile/index.jsx b/frontend/src/routes/mobile/index.jsx index a342752..29d7111 100644 --- a/frontend/src/routes/mobile/index.jsx +++ b/frontend/src/routes/mobile/index.jsx @@ -9,7 +9,6 @@ import Members from '@/pages/mobile/members/Members'; import MembersPreview from '@/pages/mobile/members/MembersPreview'; import Schedule from '@/pages/mobile/schedule/Schedule'; import ScheduleDetail from '@/pages/mobile/schedule/ScheduleDetail'; -import Birthday from '@/pages/mobile/schedule/Birthday'; import Album from '@/pages/mobile/album/Album'; import AlbumDetail from '@/pages/mobile/album/AlbumDetail'; import TrackDetail from '@/pages/mobile/album/TrackDetail'; @@ -55,7 +54,6 @@ export default function MobileRoutes() { } /> } /> - } /> } /> } /> } /> - } /> } /> } /> } />