refactor: 생일 페이지 라우트를 /schedule/:id 형식으로 변경

- /birthday/:memberName/:year → /schedule/birthday-{year}-{nameEn}
- ScheduleDetail에서 특수 ID(birthday, debut, anniversary) 감지
- Birthday 컴포넌트가 props로 year, nameEn 받도록 변경
- 멤버 API가 영문명으로도 조회 가능하도록 수정

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
caadiq 2026-01-25 13:15:04 +09:00
parent e1ee0b47a0
commit f97c925fba
9 changed files with 100 additions and 41 deletions

View file

@ -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;

View file

@ -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 (
<div className="min-h-screen flex items-center justify-center bg-gray-50 px-6">
<div className="text-center">
@ -128,7 +126,7 @@ function MobileBirthday() {
<div className="text-center py-8">
<div className="text-5xl mb-3">🎁</div>
<p className="text-gray-500 text-sm">
{year} {decodedMemberName} 생일카페 정보가 준비 중입니다
{year} {member?.name} 생일카페 정보가 준비 중입니다
</p>
<p className="text-gray-400 text-xs mt-1">생일카페 정보가 등록되면 이곳에 표시됩니다</p>
</div>

View file

@ -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}`)}
/>
);
}

View file

@ -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 <Birthday year={specialId.year} nameEn={specialId.nameEn} />;
}
//
useEffect(() => {
document.documentElement.classList.add('mobile-layout');

View file

@ -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 (
<div className="min-h-[calc(100vh-64px)] flex items-center justify-center">
<div className="text-center">
@ -125,7 +123,7 @@ function PCBirthday() {
<div className="text-center py-12">
<div className="text-6xl mb-4">🎁</div>
<p className="text-gray-500 text-lg">
{year} {decodedMemberName} 생일카페 정보가 준비 중입니다
{year} {member?.name} 생일카페 정보가 준비 중입니다
</p>
<p className="text-gray-400 text-sm mt-2">생일카페 정보가 등록되면 이곳에 표시됩니다</p>
</div>

View file

@ -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 {

View file

@ -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 <Birthday year={specialId.year} nameEn={specialId.nameEn} />;
}
const {
data: schedule,
isLoading,

View file

@ -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() {
}
/>
<Route path="/schedule/:id" element={<ScheduleDetail />} />
<Route path="/birthday/:memberName/:year" element={<Birthday />} />
<Route
path="/album"
element={

View file

@ -8,7 +8,6 @@ import Home from '@/pages/pc/public/home/Home';
import Members from '@/pages/pc/public/members/Members';
import Schedule from '@/pages/pc/public/schedule/Schedule';
import ScheduleDetail from '@/pages/pc/public/schedule/ScheduleDetail';
import Birthday from '@/pages/pc/public/schedule/Birthday';
import Album from '@/pages/pc/public/album/Album';
import AlbumDetail from '@/pages/pc/public/album/AlbumDetail';
import TrackDetail from '@/pages/pc/public/album/TrackDetail';
@ -30,7 +29,6 @@ export default function PublicRoutes() {
<Route path="/members" element={<Members />} />
<Route path="/schedule" element={<Schedule />} />
<Route path="/schedule/:id" element={<ScheduleDetail />} />
<Route path="/birthday/:memberName/:year" element={<Birthday />} />
<Route path="/album" element={<Album />} />
<Route path="/album/:name" element={<AlbumDetail />} />
<Route path="/album/:name/track/:trackTitle" element={<TrackDetail />} />