From 0be074e19fa52b619edc3ddc6bbfe05422f19ec1 Mon Sep 17 00:00:00 2001 From: hywznn Date: Fri, 15 Aug 2025 23:46:00 +0900 Subject: [PATCH 01/37] =?UTF-8?q?doc=20:=20deploy=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy-release.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/deploy-release.yml b/.github/workflows/deploy-release.yml index 4c8855d..878d135 100644 --- a/.github/workflows/deploy-release.yml +++ b/.github/workflows/deploy-release.yml @@ -34,9 +34,8 @@ jobs: - name: Build env: - # 필요 시 Vite 환경변수를 추가로 전달하세요 (예: VITE_API_BASE_URL 등) - # VITE_API_BASE_URL: ${{ secrets.VITE_API_BASE_URL }} NODE_OPTIONS: --max_old_space_size=4096 + VITE_API_BASE_URL: ${{ secrets.VITE_API_BASE_URL }} run: npm run build - name: Upload build artifact @@ -98,3 +97,6 @@ jobs: - name: Output deployment info run: | echo "Deployed to s3://$S3_BUCKET (region: $AWS_REGION)" + if [ -n "$CLOUDFRONT_DISTRIBUTION_ID" ]; then + echo "CloudFront invalidation requested for distribution: $CLOUDFRONT_DISTRIBUTION_ID" + fi From f03d13eaa443ed3c775fdf42aeabaa0311c627aa Mon Sep 17 00:00:00 2001 From: hywznn Date: Wed, 20 Aug 2025 01:59:12 +0900 Subject: [PATCH 02/37] Merge remote-tracking branch 'origin/main' into release From eced7c44b67426dfb2bb0e8415f9938b920d5ddc Mon Sep 17 00:00:00 2001 From: hywznn Date: Wed, 20 Aug 2025 02:09:36 +0900 Subject: [PATCH 03/37] =?UTF-8?q?fix=20:=20WebSocketService=EC=97=90=20sen?= =?UTF-8?q?dReadStatus=20=EB=A9=94=EC=84=9C=EB=93=9C=20=EC=B6=94=EA=B0=80?= =?UTF-8?q?=ED=95=98=EC=97=AC=20TypeScript=20=EC=98=A4=EB=A5=98=20?= =?UTF-8?q?=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/services/WebSocketService.ts | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/services/WebSocketService.ts b/src/services/WebSocketService.ts index ba1b294..a74102b 100644 --- a/src/services/WebSocketService.ts +++ b/src/services/WebSocketService.ts @@ -404,6 +404,25 @@ class WebSocketService { console.log("마지막 읽음 전송:", { roomId, messageId, roomType }); } + // 읽음 상태 전송 함수 (usePrivateChat에서 사용) + sendReadStatus(roomId: string, messageId: number): void { + if (!this.stompClient || !this.stompClient.connected) { + console.error("WebSocket이 연결되지 않았습니다."); + return; + } + + const destination = `/app/chat/private.readStatus/${roomId}`; + + this.stompClient.publish({ + destination, + body: messageId.toString(), + headers: { + "content-type": "text/plain;charset=UTF-8", + }, + }); + console.log("읽음 상태 전송:", { roomId, messageId }); + } + isConnected(): boolean { return this.stompClient?.connected || false; } From 91598658e12d11273a81833d9e85f90874588591 Mon Sep 17 00:00:00 2001 From: hywznn Date: Wed, 20 Aug 2025 22:55:30 +0900 Subject: [PATCH 04/37] Merge remote-tracking branch 'origin/main' into release From 1ea81d2d3c375dfd53a1ef867f896ff96ce37088 Mon Sep 17 00:00:00 2001 From: hywznn Date: Thu, 21 Aug 2025 00:10:27 +0900 Subject: [PATCH 05/37] =?UTF-8?q?feat=20:=20=EB=B0=B4=EB=94=94=20=EC=9D=B8?= =?UTF-8?q?=EC=8A=A4=ED=83=80=20=EC=9C=A0=ED=8A=9C=EB=B8=8C=20=EB=AA=A8?= =?UTF-8?q?=EB=91=90=20=EC=97=B0=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/Home/_components/BandInfoModal.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/Home/_components/BandInfoModal.tsx b/src/pages/Home/_components/BandInfoModal.tsx index f44b76d..ab73113 100644 --- a/src/pages/Home/_components/BandInfoModal.tsx +++ b/src/pages/Home/_components/BandInfoModal.tsx @@ -120,13 +120,13 @@ const BandInfoModal: React.FC = ({ Comp: Youtube, color: "gray-700", link: youtubeUrl, - hasLink: !!youtubeUrl, + hasLink: "https://www.youtube.com/@Banddy79", }, { Comp: Instagram, color: "gray-700", link: instagramUrl, - hasLink: !!instagramUrl, + hasLink: "https://www.instagram.com/banddy79", }, { Comp: Tictok, From b42e471735db445dea912811a42c1aca08011f8f Mon Sep 17 00:00:00 2001 From: hywznn Date: Thu, 21 Aug 2025 00:11:44 +0900 Subject: [PATCH 06/37] =?UTF-8?q?feat=20:=20=EB=AA=A8=EB=93=A0=20=EB=B0=B4?= =?UTF-8?q?=EB=93=9C=20=EC=9C=A0=ED=8A=9C=EB=B8=8C=20=EC=9D=B8=EC=8A=A4?= =?UTF-8?q?=ED=83=80=20=EB=B0=B4=EB=94=94=EB=A1=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/Home/_components/BandInfoModal.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/pages/Home/_components/BandInfoModal.tsx b/src/pages/Home/_components/BandInfoModal.tsx index ab73113..90757e9 100644 --- a/src/pages/Home/_components/BandInfoModal.tsx +++ b/src/pages/Home/_components/BandInfoModal.tsx @@ -119,14 +119,14 @@ const BandInfoModal: React.FC = ({ { Comp: Youtube, color: "gray-700", - link: youtubeUrl, - hasLink: "https://www.youtube.com/@Banddy79", + link: "https://www.youtube.com/@Banddy79", + hasLink: true, }, { Comp: Instagram, color: "gray-700", - link: instagramUrl, - hasLink: "https://www.instagram.com/banddy79", + link: "https://www.instagram.com/banddy79", + hasLink: true, }, { Comp: Tictok, From 5122253c8be26ef15d3a5efd854b9d8f0a454142 Mon Sep 17 00:00:00 2001 From: hywznn Date: Thu, 21 Aug 2025 00:16:28 +0900 Subject: [PATCH 07/37] Merge remote-tracking branch 'origin/main' into release From 8947314adcc30cc9cdea37431a2391d93a04605e Mon Sep 17 00:00:00 2001 From: hywznn Date: Thu, 21 Aug 2025 00:17:59 +0900 Subject: [PATCH 08/37] =?UTF-8?q?feat=20:=20=EB=AA=A8=EB=93=A0=20=EB=B0=B4?= =?UTF-8?q?=EB=93=9C=20=ED=99=88=EB=AA=A8=EB=8B=AC=20=EC=9D=B8=EC=8A=A4?= =?UTF-8?q?=ED=83=80=20=EC=9C=A0=ED=8A=9C=EB=B8=8C=20=EB=B0=B4=EB=94=94=20?= =?UTF-8?q?=EA=B3=84=EC=A0=95=EC=9C=BC=EB=A1=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/Home/HomePage.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/Home/HomePage.tsx b/src/pages/Home/HomePage.tsx index 28dcda3..9f3f42e 100644 --- a/src/pages/Home/HomePage.tsx +++ b/src/pages/Home/HomePage.tsx @@ -632,12 +632,12 @@ const HomePage = () => { youtubeUrl={ selectedBand?.profileData?.sns?.find( (s) => s.platform === "youtube" - )?.url || "https://youtube.com" + )?.url || "https://www.youtube.com/@Banddy79" } instagramUrl={ selectedBand?.profileData?.sns?.find( (s) => s.platform === "instagram" - )?.url || "/www.instagram.com/banddy79?igsh=NmhvNWlyc3gxNnlk" + )?.url || "https://www.instagram.com/banddy79/" } bandId={selectedBand?.id?.toString()} // 추가 /> From 86f98913c7c7cf83f30dfab7844f5763d64420cb Mon Sep 17 00:00:00 2001 From: hywznn Date: Thu, 21 Aug 2025 00:31:32 +0900 Subject: [PATCH 09/37] =?UTF-8?q?fix=20:=20=EB=A7=81=ED=81=AC=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/Home/_components/BandInfoModal.tsx | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/pages/Home/_components/BandInfoModal.tsx b/src/pages/Home/_components/BandInfoModal.tsx index 90757e9..557f2a8 100644 --- a/src/pages/Home/_components/BandInfoModal.tsx +++ b/src/pages/Home/_components/BandInfoModal.tsx @@ -160,16 +160,20 @@ const BandInfoModal: React.FC = ({ ); } else { + const absoluteLink = link.startsWith("http") + ? link + : `https://${link.replace(/^\/+/, "")}`; return ( - + window.open(absoluteLink, "_blank", "noopener,noreferrer") + } style={{ display: "inline-block" }} > {iconElement} - + ); } } else { From d5a11e51e9a7ab38994c21d367457ed3837cd5ea Mon Sep 17 00:00:00 2001 From: hywznn Date: Thu, 21 Aug 2025 01:30:32 +0900 Subject: [PATCH 10/37] =?UTF-8?q?fix=20:=20=EC=A7=84=EC=A7=9C=20=EC=9D=B8?= =?UTF-8?q?=EC=8A=A4=ED=83=80=20=EC=9C=A0=ED=8A=9C=EB=B8=8C=20=EB=B0=B4?= =?UTF-8?q?=EB=94=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/Home/_components/BandInfoModal.tsx | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/pages/Home/_components/BandInfoModal.tsx b/src/pages/Home/_components/BandInfoModal.tsx index 557f2a8..e2b6e0c 100644 --- a/src/pages/Home/_components/BandInfoModal.tsx +++ b/src/pages/Home/_components/BandInfoModal.tsx @@ -76,6 +76,14 @@ const BandInfoModal: React.FC = ({ return MicImg; }; + const YOUTUBE_FALLBACK = "https://www.youtube.com/@Banddy79"; + const INSTAGRAM_FALLBACK = "https://www.instagram.com/banddy79/"; + + const resolvedYoutubeLink = + youtubeUrl && youtubeUrl.trim() ? youtubeUrl : YOUTUBE_FALLBACK; + const resolvedInstagramLink = + instagramUrl && instagramUrl.trim() ? instagramUrl : INSTAGRAM_FALLBACK; + return (
= ({ { Comp: Youtube, color: "gray-700", - link: "https://www.youtube.com/@Banddy79", + link: resolvedYoutubeLink, hasLink: true, }, { Comp: Instagram, color: "gray-700", - link: "https://www.instagram.com/banddy79", + link: resolvedInstagramLink, hasLink: true, }, { From ef27fc796cf83a8306a0adeb31116c77e3cda76a Mon Sep 17 00:00:00 2001 From: hywznn Date: Thu, 21 Aug 2025 02:47:30 +0900 Subject: [PATCH 11/37] =?UTF-8?q?store:=20=EB=AA=A8=EC=A7=91=EC=A4=91=20?= =?UTF-8?q?=EB=B0=B4=EB=93=9C=20ID=20=EC=9C=A0=ED=8B=B8=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/Home/HomePage.tsx | 206 ++++++++++++++++++------------------ src/store/userStore.ts | 58 +++++++++- 2 files changed, 158 insertions(+), 106 deletions(-) diff --git a/src/pages/Home/HomePage.tsx b/src/pages/Home/HomePage.tsx index 9f3f42e..1665f7d 100644 --- a/src/pages/Home/HomePage.tsx +++ b/src/pages/Home/HomePage.tsx @@ -6,8 +6,9 @@ import MuiDialog from "@/shared/components/MuiDialog"; import BandInfoModal from "./_components/BandInfoModal"; import { getRecommendedFromSimilar, + getBandDetail, probeSomeBandDetails, - getAllBands, + getRecruitingBandIds, } from "@/store/userStore"; import { useRecommendedBands } from "@/features/band/hooks/useBandData"; import type { BandDetail } from "@/types/band"; @@ -247,44 +248,35 @@ const HomePage = () => { profiles = (await getRecommendedFromSimilar()) as BandProfileData[]; } - // 선택적 보강: 전체 밴드 목록을 조회해 전 범위(candidateIds) 구성 - let candidateIds: number[] | undefined; + let details: BandDetail[] = []; try { - const allBands = await getAllBands(); - const ids = Array.isArray(allBands) - ? allBands - .map((b) => Number(b?.bandId ?? b?.id)) - .filter((n: number) => Number.isFinite(n)) - : []; - if (ids.length > 0) { - candidateIds = Array.from(new Set(ids)); - } - } catch { - // 서버 미구현/오류 시 무시하고 fallback 사용 - } + let allDetails: BandDetail[] = []; + // 1) RECRUITING 밴드 ID 우선 상세 조회 + try { + const recruitingIds = await getRecruitingBandIds(); + if (recruitingIds && recruitingIds.length > 0) { + const detailResults = await Promise.all( + recruitingIds.map((id) => + getBandDetail(String(id)).catch(() => null) + ) + ); + allDetails = detailResults.filter(Boolean) as BandDetail[]; + } + } catch {} - const fallbackIds = [ - 49, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, - 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, - 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 50, 51, 52, 53, 54, 55, 56, - 57, 58, 59, 60, - ]; + // 2) 백업: 유사 기반 상세 수집 + if (allDetails.length === 0) { + allDetails = await probeSomeBandDetails({ limit: 40 }); + } - let details: BandDetail[] = []; - try { - const baseDetails = await probeSomeBandDetails({ - limit: Math.min(100, candidateIds?.length ?? 40), - candidateIds: - candidateIds && candidateIds.length > 0 - ? candidateIds - : fallbackIds, - }); // 모집 공고 상태 확인: RECRUITING만 유지 const filtered = await Promise.all( - baseDetails.map(async (d) => { + allDetails.map(async (d) => { try { - const recruit = await getBandRecruitDetail(String(d.bandId)); - return recruit?.status === "RECRUITING" ? d : null; + const recruitRes = await getBandRecruitDetail(String(d.bandId)); + if (!recruitRes || recruitRes.isSuccess !== true) return null; + const status = recruitRes.result?.status; + return status === "RECRUITING" ? d : null; } catch { return null; } @@ -292,9 +284,8 @@ const HomePage = () => { ); details = filtered.filter(Boolean) as BandDetail[]; } catch (error) { - // probeSomeBandDetails 실패 시 빈 배열 사용 if (import.meta.env.DEV) { - console.warn("밴드 상세정보 조회 실패, 빈 배열 사용"); + console.warn("밴드 상세정보 전체 조회 실패, 빈 배열 사용"); console.error("상세 에러 정보:", error); } details = []; @@ -321,76 +312,85 @@ const HomePage = () => { return; } - // 전체 추천 목록을 유지하고, 상세가 있으면 보강만 적용 - const paired = validProfiles.map((profile, index) => ({ - profile, - detail: details[index], - index, - })); - - const bands: Band[] = paired.map(({ profile, detail, index }) => { - // API 응답 구조에 따라 안전하게 접근 - const goalTracks = profile.goalTracks || []; - const preferredArtists = profile.preferredArtists || []; - const sessions = profile.sessions || []; - - // 첫 번째 곡을 대표 이미지로 사용 - const representativeTrack = goalTracks[0]; - const representativeArtist = preferredArtists[0]; - - // 세션이 비어있으면 기본 태그 사용 - const tags = - sessions.length > 0 - ? sessions.map((session: string) => cleanSessionName(session)) - : fallbackBandData[index]?.tags || [ - "기타 모집", - "YOASOBI", - "J-POP", - "aiko", - ]; - - // 모든 데이터가 비어있으면 fallback 데이터 사용 - const hasValidData = - goalTracks.length > 0 || - preferredArtists.length > 0 || - sessions.length > 0; - const fallbackBand = fallbackBandData[index]; - - if (!hasValidData && fallbackBand) { - return fallbackBand; + // 모집 공고(RECRUITING & isSuccess)로 확정된 상세만으로 캐러셀 구성 + const recruitingPairs: Array<{ detail: BandDetail; recruit: any }> = []; + for (const d of details) { + try { + const recruitRes = await getBandRecruitDetail(String(d.bandId)); + if ( + recruitRes && + recruitRes.isSuccess === true && + recruitRes.result?.status === "RECRUITING" + ) { + recruitingPairs.push({ detail: d, recruit: recruitRes.result }); + } + } catch { + // skip } + } - return { - id: Number((detail as Partial)?.bandId) || index + 1, - image: - (detail as Partial)?.profileImageUrl || - representativeTrack?.imageUrl || - representativeArtist?.imageUrl || - fallbackBandData[index]?.image || - homeAlbum3Img, - title: - (detail as Partial)?.bandName || - representativeTrack?.title || - representativeArtist?.name || - fallbackBandData[index]?.title || - "그래요 저 왜색 짙어요", - subtitle: - representativeTrack?.artist || - representativeArtist?.name || - fallbackBandData[index]?.subtitle || - "혼또니 아리가또 고자이마스", - tags, - profileData: profile, // 원본 데이터 저장 - bandName: (detail as Partial)?.bandName, - // 신규 스펙 반영: 대표 음원 파일 URL 전달 (없으면 null) - representativeSongFileUrl: - ( - detail as Partial & { - representativeSongFile?: { fileUrl?: string }; - } - )?.representativeSongFile?.fileUrl ?? null, - }; - }); + const bands: Band[] = recruitingPairs.map( + ({ detail, recruit }, index) => { + const goalTracks = Array.isArray(recruit?.tracks) + ? recruit.tracks.map((t: any) => ({ + title: String(t?.title || ""), + artist: "", + imageUrl: String(t?.imageUrl || ""), + })) + : []; + const preferredArtists = Array.isArray(recruit?.artists) + ? recruit.artists.map((a: any) => ({ + name: String(a?.name || ""), + imageUrl: String(a?.imageUrl || ""), + })) + : []; + const composition = { + averageAge: String(recruit?.averageAge || ""), + maleCount: Number(recruit?.maleCount || 0), + femaleCount: Number(recruit?.femaleCount || 0), + }; + const sessions = Array.isArray(recruit?.sessions) + ? recruit.sessions + : []; + + const representativeTrack = goalTracks[0]; + const representativeArtist = preferredArtists[0]; + + const tags = (sessions.length > 0 + ? sessions.map((session: string) => cleanSessionName(session)) + : fallbackBandData[index]?.tags) || [ + "기타 모집", + "YOASOBI", + "J-POP", + "aiko", + ]; + + return { + id: Number(detail.bandId), + image: + detail.profileImageUrl || + String(recruit?.profileImageUrl || "") || + representativeTrack?.imageUrl || + representativeArtist?.imageUrl || + fallbackBandData[index]?.image || + homeAlbum3Img, + title: detail.bandName, + subtitle: String(detail.description || recruit?.description || ""), + tags, + profileData: { + goalTracks, + preferredArtists, + composition, + sns: [], + sessions, + jobs: Array.isArray(recruit?.jobs) ? recruit.jobs : [], + }, + bandName: detail.bandName, + representativeSongFileUrl: + String(recruit?.representativeSongFile?.fileUrl || "") || null, + }; + } + ); // memberId 36/37 계정에서 bandId 49를 캐러셀에 보장 노출 try { diff --git a/src/store/userStore.ts b/src/store/userStore.ts index 986f3bb..a048380 100644 --- a/src/store/userStore.ts +++ b/src/store/userStore.ts @@ -215,11 +215,22 @@ export const getBandDetail = async ( // 신규 상세 스펙(모집 공고) 조회 API export const getBandRecruitDetail = async ( bandId: string -): Promise => { +): Promise<{ + isSuccess: boolean; + result: BandRecruitDetail | null; + message?: string; +} | null> => { try { const res = await API.get(API_ENDPOINTS.RECRUITMENT.DETAIL(bandId)); - const data = (res.data?.result || res.data) as Partial; - return (data || null) as BandRecruitDetail; + const isSuccess: boolean = Boolean(res?.data?.isSuccess ?? true); + const result = (res.data?.result || + null) as Partial | null; + const message = (res?.data?.message as string | undefined) ?? undefined; + return { + isSuccess, + result: (result || null) as BandRecruitDetail | null, + message, + }; } catch (error) { console.warn("밴드 모집 상세 조회 실패:", { bandId, error }); return null; @@ -379,6 +390,47 @@ export const getBandArtists = async (bandId: string) => { } }; +// 모집중 밴드 ID 조회 (재사용 가능): +// 1) 환경변수 VITE_RECRUITING_BAND_IDS="4,10,11" 우선 사용 +// 2) 서버에 /api/recruitments?status=RECRUITING 지원 시 시도 (GET 미지원이면 무시) +// 3) 마지막으로 하드코딩된 임시 목록(운영 전환 시 제거) +export const getRecruitingBandIds = async (): Promise => { + // 1) ENV 우선 + const envIdsRaw = (import.meta as any)?.env?.VITE_RECRUITING_BAND_IDS as + | string + | undefined; + if (typeof envIdsRaw === "string" && envIdsRaw.trim().length > 0) { + return envIdsRaw + .split(",") + .map((s) => Number(s.trim())) + .filter((n) => Number.isFinite(n)); + } + + // 2) 서버 목록 시도 (405 등 에러시 무시) + try { + const res = await API.get(API_ENDPOINTS.BANDS.RECRUIT, { + params: { status: "RECRUITING", page: 0, size: 1000 }, + }); + const list = Array.isArray(res?.data) + ? res.data + : Array.isArray(res?.data?.result) + ? res.data.result + : []; + const ids = list + .map((r: any) => Number(r?.bandId ?? r?.id)) + .filter((n: number) => Number.isFinite(n)); + if (ids.length > 0) return Array.from(new Set(ids)); + } catch { + // ignore + } + + // 3) 임시 하드코딩 (운영 API 준비 전까지만 사용) + return [ + 4, 10, 11, 12, 13, 14, 15, 16, 17, 18, 28, 42, 45, 49, 51, 63, 64, 65, 67, + 68, + ]; +}; + // 모든 밴드 목록 조회 API export const getAllBands = async () => { try { From 9fe01ba6a273cb26d8b2a3b256633b9eb2f17971 Mon Sep 17 00:00:00 2001 From: hywznn Date: Thu, 21 Aug 2025 03:32:19 +0900 Subject: [PATCH 12/37] =?UTF-8?q?=ED=99=88:=20=EB=A6=AC=EC=BF=A0=EB=A5=B4?= =?UTF-8?q?=ED=8C=85=20=EB=AA=A9=EB=A1=9D=20=EC=A7=81=EC=A0=91=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20=EB=B0=8F=20=EC=BA=90=EC=8B=9C=20=EC=A0=81=EC=9A=A9?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EB=A1=9C=EB=94=A9=20=EC=B5=9C=EC=A0=81?= =?UTF-8?q?=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/Home/HomePage.tsx | 193 +++++++++++++----------------------- src/store/userStore.ts | 109 ++++++++++++++++++++ 2 files changed, 177 insertions(+), 125 deletions(-) diff --git a/src/pages/Home/HomePage.tsx b/src/pages/Home/HomePage.tsx index 1665f7d..4ac56b3 100644 --- a/src/pages/Home/HomePage.tsx +++ b/src/pages/Home/HomePage.tsx @@ -6,13 +6,10 @@ import MuiDialog from "@/shared/components/MuiDialog"; import BandInfoModal from "./_components/BandInfoModal"; import { getRecommendedFromSimilar, - getBandDetail, - probeSomeBandDetails, - getRecruitingBandIds, + getRecruitingBandSummaries, } from "@/store/userStore"; import { useRecommendedBands } from "@/features/band/hooks/useBandData"; -import type { BandDetail } from "@/types/band"; -import { getBandRecruitDetail } from "@/store/userStore"; +import type {} from "@/types/band"; import { createGroupChat } from "@/store/chatApi"; import { API } from "@/api/API"; import { API_ENDPOINTS } from "@/constants"; @@ -248,48 +245,13 @@ const HomePage = () => { profiles = (await getRecommendedFromSimilar()) as BandProfileData[]; } - let details: BandDetail[] = []; - try { - let allDetails: BandDetail[] = []; - // 1) RECRUITING 밴드 ID 우선 상세 조회 - try { - const recruitingIds = await getRecruitingBandIds(); - if (recruitingIds && recruitingIds.length > 0) { - const detailResults = await Promise.all( - recruitingIds.map((id) => - getBandDetail(String(id)).catch(() => null) - ) - ); - allDetails = detailResults.filter(Boolean) as BandDetail[]; - } - } catch {} - - // 2) 백업: 유사 기반 상세 수집 - if (allDetails.length === 0) { - allDetails = await probeSomeBandDetails({ limit: 40 }); - } - - // 모집 공고 상태 확인: RECRUITING만 유지 - const filtered = await Promise.all( - allDetails.map(async (d) => { - try { - const recruitRes = await getBandRecruitDetail(String(d.bandId)); - if (!recruitRes || recruitRes.isSuccess !== true) return null; - const status = recruitRes.result?.status; - return status === "RECRUITING" ? d : null; - } catch { - return null; - } - }) - ); - details = filtered.filter(Boolean) as BandDetail[]; - } catch (error) { - if (import.meta.env.DEV) { - console.warn("밴드 상세정보 전체 조회 실패, 빈 배열 사용"); - console.error("상세 에러 정보:", error); - } - details = []; - } + // 서버 리쿠르팅 목록을 직접 가져와 카드로 구성 (상세 다중 호출 제거) + const recruitingSummaries = await getRecruitingBandSummaries({ + page: 0, + size: 50, + useCache: true, + cacheMs: 60 * 1000, + }); // profiles가 빈 배열이거나 undefined인 경우 기본 데이터 사용 if (!profiles || profiles.length === 0) { @@ -312,85 +274,66 @@ const HomePage = () => { return; } - // 모집 공고(RECRUITING & isSuccess)로 확정된 상세만으로 캐러셀 구성 - const recruitingPairs: Array<{ detail: BandDetail; recruit: any }> = []; - for (const d of details) { - try { - const recruitRes = await getBandRecruitDetail(String(d.bandId)); - if ( - recruitRes && - recruitRes.isSuccess === true && - recruitRes.result?.status === "RECRUITING" - ) { - recruitingPairs.push({ detail: d, recruit: recruitRes.result }); - } - } catch { - // skip - } - } + // 모집 공고(RECRUITING) 요약만으로 캐러셀 구성 + const bands: Band[] = recruitingSummaries.map((recruit: any, index) => { + const goalTracks = Array.isArray(recruit?.tracks) + ? recruit.tracks.map((t: any) => ({ + title: String(t?.title || ""), + artist: "", + imageUrl: String(t?.imageUrl || ""), + })) + : []; + const preferredArtists = Array.isArray(recruit?.artists) + ? recruit.artists.map((a: any) => ({ + name: String(a?.name || ""), + imageUrl: String(a?.imageUrl || ""), + })) + : []; + const composition = { + averageAge: String(recruit?.averageAge || ""), + maleCount: Number(recruit?.maleCount || 0), + femaleCount: Number(recruit?.femaleCount || 0), + }; + const sessions = Array.isArray(recruit?.sessions) + ? recruit.sessions + : []; - const bands: Band[] = recruitingPairs.map( - ({ detail, recruit }, index) => { - const goalTracks = Array.isArray(recruit?.tracks) - ? recruit.tracks.map((t: any) => ({ - title: String(t?.title || ""), - artist: "", - imageUrl: String(t?.imageUrl || ""), - })) - : []; - const preferredArtists = Array.isArray(recruit?.artists) - ? recruit.artists.map((a: any) => ({ - name: String(a?.name || ""), - imageUrl: String(a?.imageUrl || ""), - })) - : []; - const composition = { - averageAge: String(recruit?.averageAge || ""), - maleCount: Number(recruit?.maleCount || 0), - femaleCount: Number(recruit?.femaleCount || 0), - }; - const sessions = Array.isArray(recruit?.sessions) - ? recruit.sessions - : []; - - const representativeTrack = goalTracks[0]; - const representativeArtist = preferredArtists[0]; - - const tags = (sessions.length > 0 - ? sessions.map((session: string) => cleanSessionName(session)) - : fallbackBandData[index]?.tags) || [ - "기타 모집", - "YOASOBI", - "J-POP", - "aiko", - ]; - - return { - id: Number(detail.bandId), - image: - detail.profileImageUrl || - String(recruit?.profileImageUrl || "") || - representativeTrack?.imageUrl || - representativeArtist?.imageUrl || - fallbackBandData[index]?.image || - homeAlbum3Img, - title: detail.bandName, - subtitle: String(detail.description || recruit?.description || ""), - tags, - profileData: { - goalTracks, - preferredArtists, - composition, - sns: [], - sessions, - jobs: Array.isArray(recruit?.jobs) ? recruit.jobs : [], - }, - bandName: detail.bandName, - representativeSongFileUrl: - String(recruit?.representativeSongFile?.fileUrl || "") || null, - }; - } - ); + const representativeTrack = goalTracks[0]; + const representativeArtist = preferredArtists[0]; + + const tags = (sessions.length > 0 + ? sessions.map((session: string) => cleanSessionName(session)) + : fallbackBandData[index]?.tags) || [ + "기타 모집", + "YOASOBI", + "J-POP", + "aiko", + ]; + + return { + id: Number(recruit?.bandId), + image: + String(recruit?.profileImageUrl || "") || + representativeTrack?.imageUrl || + representativeArtist?.imageUrl || + fallbackBandData[index]?.image || + homeAlbum3Img, + title: String(recruit?.name || `밴드 ${recruit?.bandId ?? ""}`), + subtitle: String(recruit?.description || ""), + tags, + profileData: { + goalTracks, + preferredArtists, + composition, + sns: [], + sessions, + jobs: Array.isArray(recruit?.jobs) ? recruit.jobs : [], + }, + bandName: String(recruit?.name || ""), + representativeSongFileUrl: + String(recruit?.representativeSongFile?.fileUrl || "") || null, + } as Band; + }); // memberId 36/37 계정에서 bandId 49를 캐러셀에 보장 노출 try { diff --git a/src/store/userStore.ts b/src/store/userStore.ts index a048380..f0355cc 100644 --- a/src/store/userStore.ts +++ b/src/store/userStore.ts @@ -237,6 +237,115 @@ export const getBandRecruitDetail = async ( } }; +// 홈 전용: 리쿠르팅 목록 캐시 (간단 TTL 캐시) +let recruitingListCache: { + expiresAt: number; + data: Array>; +} | null = null; + +export const getRecruitingBandSummaries = async (options?: { + page?: number; + size?: number; + useCache?: boolean; + cacheMs?: number; +}): Promise>> => { + const page = options?.page ?? 0; + const size = options?.size ?? 40; + const useCache = options?.useCache !== false; + const cacheMs = options?.cacheMs ?? 60 * 1000; + + if ( + useCache && + recruitingListCache && + recruitingListCache.expiresAt > Date.now() + ) { + return recruitingListCache.data; + } + + const normalize = (item: any) => ({ + bandId: Number(item?.bandId ?? item?.id ?? 0) || undefined, + name: item?.name ?? item?.bandName ?? undefined, + description: item?.description ?? undefined, + profileImageUrl: item?.profileImageUrl ?? item?.imageUrl ?? undefined, + sessions: Array.isArray(item?.sessions) ? item.sessions : [], + artists: Array.isArray(item?.artists) ? item.artists : [], + tracks: Array.isArray(item?.tracks) ? item.tracks : [], + jobs: Array.isArray(item?.jobs) ? item.jobs : [], + averageAge: item?.averageAge, + maleCount: item?.maleCount, + femaleCount: item?.femaleCount, + representativeSongFile: item?.representativeSongFile ?? null, + status: item?.status, + }); + + const viaIdsFallback = async () => { + try { + const ids = (await getRecruitingBandIds())?.slice(0, size) ?? []; + const batchSize = 8; + const results: any[] = []; + for (let i = 0; i < ids.length; i += batchSize) { + const batch = ids.slice(i, i + batchSize); + const batchResults = await Promise.all( + batch.map(async (id) => { + const recruitRes = await getBandRecruitDetail(String(id)); + const recruit = recruitRes?.result; + if (recruitRes?.isSuccess && recruit?.status === "RECRUITING") { + return normalize({ ...recruit, bandId: id }); + } + return null; + }) + ); + results.push(...batchResults.filter(Boolean)); + } + + if (useCache) { + recruitingListCache = { + expiresAt: Date.now() + cacheMs, + data: results, + }; + } + return results; + } catch (e) { + console.warn("리쿠르팅 목록 ID 폴백 실패:", e); + return []; + } + }; + + try { + const res = await API.get(API_ENDPOINTS.BANDS.RECRUIT, { + params: { status: "RECRUITING", page, size }, + }); + const list = Array.isArray(res?.data) + ? res.data + : Array.isArray(res?.data?.result) + ? res.data.result + : []; + + // 서버가 GET을 지원하지 않거나 빈 리스트면 폴백 + if (!Array.isArray(list) || list.length === 0) { + return await viaIdsFallback(); + } + + const normalized: Array> = list.map(normalize); + + if (useCache) { + recruitingListCache = { + expiresAt: Date.now() + cacheMs, + data: normalized, + }; + } + return normalized; + } catch (error: any) { + // 405/404 등은 자연스럽게 폴백 + const status = error?.response?.status; + if (status === 405 || status === 404) { + return await viaIdsFallback(); + } + console.warn("리쿠르팅 목록 조회 실패:", error); + return await viaIdsFallback(); + } +}; + // 추천 밴드 목록 조회 API (홈페이지용) export const getRecommendedBands = async () => { try { From 41c3f990824b312cdb8164bc5714d7d930654c96 Mon Sep 17 00:00:00 2001 From: hywznn Date: Thu, 21 Aug 2025 03:35:53 +0900 Subject: [PATCH 13/37] =?UTF-8?q?feat:=20=EB=B0=B4=EB=93=9C=20=EC=A1=B0?= =?UTF-8?q?=EC=9D=B8=20=EC=8B=9C=20=EB=B9=84=EB=AA=A8=EC=A7=91=20=EC=84=B8?= =?UTF-8?q?=EC=85=98(BAND=5FSESSION4000)=20=EB=AA=A8=EB=8B=AC=20=EC=95=88?= =?UTF-8?q?=EB=82=B4=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/Home/_components/ButtonSection.tsx | 38 ++++++++++++++++++-- 1 file changed, 36 insertions(+), 2 deletions(-) diff --git a/src/pages/Home/_components/ButtonSection.tsx b/src/pages/Home/_components/ButtonSection.tsx index bf91b68..39fbb84 100644 --- a/src/pages/Home/_components/ButtonSection.tsx +++ b/src/pages/Home/_components/ButtonSection.tsx @@ -39,6 +39,8 @@ const ButtonSection = ({ }, [isBookmarked]); const [open, setOpen] = useState(false); const [openSession, setOpenSession] = useState(false); + const [openSessionError, setOpenSessionError] = useState(false); + const [sessionErrorMessage, setSessionErrorMessage] = useState(""); const handleJoinClick = () => { // 세션 선택 모달 먼저 표시 @@ -137,12 +139,44 @@ const ButtonSection = ({ } else { navigate("/home/chat-demo"); } - } catch { - // 실패 시 기존 플로우로 대체 + } catch (err: any) { + const code = + err?.response?.data?.code ?? err?.data?.code ?? err?.code; + const msg = + err?.response?.data?.message ?? + err?.data?.message ?? + err?.message; + if (code === "BAND_SESSION4000") { + setSessionErrorMessage(msg || "해당 세션은 모집 중이 아닙니다."); + setOpenSessionError(true); + return; + } + // 기타 실패 시 기존 플로우로 대체 if (onJoinClick) onJoinClick(); } }} /> + + {/* 세션 비모집 에러 모달 */} + +
+ logo +

+ 안내 +

+

+ {sessionErrorMessage || "해당 세션은 모집 중이 아닙니다."} +

+
+ +
+
+
); }; From 5acf40ca5cefb47c918ff46856864a7a7b75a536 Mon Sep 17 00:00:00 2001 From: hywznn Date: Thu, 21 Aug 2025 03:37:41 +0900 Subject: [PATCH 14/37] =?UTF-8?q?feat=20:=20=EC=BA=90=EB=9F=AC=EC=85=80=20?= =?UTF-8?q?5=EC=B4=88=20=EC=9E=90=EB=8F=99=20=EB=84=98=EA=B9=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/Home/_components/BandCarousel.tsx | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/pages/Home/_components/BandCarousel.tsx b/src/pages/Home/_components/BandCarousel.tsx index 4911a0d..109873f 100644 --- a/src/pages/Home/_components/BandCarousel.tsx +++ b/src/pages/Home/_components/BandCarousel.tsx @@ -99,6 +99,18 @@ const BandCarousel: React.FC<{ } }, [index]); + // 5초마다 자동으로 다음 슬라이드로 이동 + useEffect(() => { + if (!bands || bands.length === 0) return; + const intervalId = window.setInterval(() => { + if (!isAnimating) { + setIsAnimating(true); + setIndex((prev) => prev + 1); + } + }, 5000); + return () => window.clearInterval(intervalId); + }, [bands.length, isAnimating]); + const handleBandClick = (band: Band) => { if (onImageClick) { onImageClick(band); From cb67b4f8e178a3c2f48376807b2c7b0e6747303d Mon Sep 17 00:00:00 2001 From: hywznn Date: Thu, 21 Aug 2025 03:41:08 +0900 Subject: [PATCH 15/37] =?UTF-8?q?feat=20:=20=EB=B0=B4=EB=93=9C=20=EB=85=B9?= =?UTF-8?q?=EC=9D=8C=ED=8C=8C=EC=9D=BC=20=ED=86=A0=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EB=A9=94=EC=84=B8=EC=A7=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/Home/_components/BandCarousel.tsx | 25 +++++++++++----- src/pages/Home/_components/ButtonSection.tsx | 31 +++++++++++++++++--- 2 files changed, 45 insertions(+), 11 deletions(-) diff --git a/src/pages/Home/_components/BandCarousel.tsx b/src/pages/Home/_components/BandCarousel.tsx index 109873f..0e4198c 100644 --- a/src/pages/Home/_components/BandCarousel.tsx +++ b/src/pages/Home/_components/BandCarousel.tsx @@ -37,14 +37,25 @@ const BandCarousel: React.FC<{ const containerRef = useRef(null); // 토스트 상태를 여기서 관리 - const [toast, setToast] = useState(false); + const [toastOpen, setToastOpen] = useState(false); + const [toastMessage, setToastMessage] = useState(""); useEffect(() => { - if (toast) { - const timer = setTimeout(() => setToast(false), 2000); + if (toastOpen) { + const timer = setTimeout(() => setToastOpen(false), 2000); return () => clearTimeout(timer); } - }, [toast]); + }, [toastOpen]); + + // 하위에서 사용할 토스트 표시 헬퍼 + const showToast = (open: boolean, text?: string) => { + if (open) { + setToastMessage(text || ""); + setToastOpen(true); + } else { + setToastOpen(false); + } + }; const handleNext = () => { if (isAnimating) return; @@ -151,7 +162,7 @@ const BandCarousel: React.FC<{

{band.subtitle}

onJoinClick(band) : undefined} @@ -174,7 +185,7 @@ const BandCarousel: React.FC<{
{/* 토스트 메시지 */} - {toast && ( + {toastOpen && (
- 밴드가 저장 되었습니다. + {toastMessage || "밴드가 저장 되었습니다."}
)} diff --git a/src/pages/Home/_components/ButtonSection.tsx b/src/pages/Home/_components/ButtonSection.tsx index 39fbb84..0ad042b 100644 --- a/src/pages/Home/_components/ButtonSection.tsx +++ b/src/pages/Home/_components/ButtonSection.tsx @@ -30,6 +30,8 @@ const ButtonSection = ({ }: ButtonSectionProps) => { const navigate = useNavigate(); const [soundOn, setSoundOn] = useState(false); + const audioRef = useState(null)[0]; + const [currentAudio, setCurrentAudio] = useState(null); const isBookmarked = useIsBookmarked(bandId); const [starOn, setStarOn] = useState(isBookmarked); const toggleBookmark = useToggleBandBookmark(); @@ -52,13 +54,34 @@ const ButtonSection = ({