From 85cc3b20804462cac0f1f717775e92b81e9c818e Mon Sep 17 00:00:00 2001 From: ikae Date: Thu, 1 Oct 2026 14:27:01 +0900 Subject: [PATCH] =?UTF-8?q?fix:=20=EC=8A=AC=EB=A1=9C=ED=94=84=EC=BA=A0=20?= =?UTF-8?q?=EC=9A=B4=EC=98=81=20=ED=99=98=EA=B2=BD=20=EC=8A=A4=ED=8A=B8?= =?UTF-8?q?=EB=A6=BC=20=EB=AF=B8=EC=B6=9C=EB=A0=A5=20=EC=9D=B4=EC=8A=88=20?= =?UTF-8?q?=EC=A1=B0=EC=B9=98=20(Dockerfile=20public=20=EB=B3=B5=EC=82=AC,?= =?UTF-8?q?=20Nginx=20=EC=B6=A9=EB=8F=8C=20=EB=B0=A9=EC=A7=80=20/stream-pr?= =?UTF-8?q?oxy/=20=EC=A0=84=ED=99=98,=20=EB=A7=88=EC=9D=B4=EA=B7=B8?= =?UTF-8?q?=EB=A0=88=EC=9D=B4=EC=85=98=20006)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy-backend.yml | 3 +- .github/workflows/gradle.yml | 3 +- .../main/resources/resort-cameras-local.json | 80 +++++++++---------- .../006_migration_fix_stream_proxy_path.sql | 58 ++++++++++++++ docs/project/work.md | 17 ++++ frontend/Dockerfile | 1 + frontend/app/resort-cam/fallback-data.ts | 68 ++++++++-------- .../high1}/[ch]/[file]/route.ts | 0 .../muju}/[cam]/[file]/route.ts | 0 .../o2}/[file]/route.ts | 0 scripts/verify-production-migration-006.sh | 41 ++++++++++ 11 files changed, 195 insertions(+), 76 deletions(-) create mode 100644 database/production/006_migration_fix_stream_proxy_path.sql rename frontend/app/{api/high1-stream => stream-proxy/high1}/[ch]/[file]/route.ts (100%) rename frontend/app/{api/muju-stream => stream-proxy/muju}/[cam]/[file]/route.ts (100%) rename frontend/app/{api/o2-stream => stream-proxy/o2}/[file]/route.ts (100%) create mode 100644 scripts/verify-production-migration-006.sh diff --git a/.github/workflows/deploy-backend.yml b/.github/workflows/deploy-backend.yml index 912190c..bc15aad 100644 --- a/.github/workflows/deploy-backend.yml +++ b/.github/workflows/deploy-backend.yml @@ -67,8 +67,9 @@ jobs: - name: Verify production migration 004 used market schema working-directory: ${{ github.workspace }} run: | - chmod +x scripts/run-production-migrations.sh scripts/verify-production-migration-005.sh + chmod +x scripts/run-production-migrations.sh scripts/verify-production-migration-005.sh scripts/verify-production-migration-006.sh scripts/verify-production-migration-005.sh + scripts/verify-production-migration-006.sh - run: ./gradlew spotlessCheck - run: ./gradlew test build -x spotlessCheck diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index 302e7b6..a74aaaf 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -103,8 +103,9 @@ jobs: - name: Verify production migration 004 used market schema working-directory: ${{ github.workspace }} run: | - chmod +x scripts/run-production-migrations.sh scripts/verify-production-migration-005.sh + chmod +x scripts/run-production-migrations.sh scripts/verify-production-migration-005.sh scripts/verify-production-migration-006.sh scripts/verify-production-migration-005.sh + scripts/verify-production-migration-006.sh - name: Build and Test with Gradle run: ./gradlew test build -x spotlessCheck diff --git a/backend/src/main/resources/resort-cameras-local.json b/backend/src/main/resources/resort-cameras-local.json index de80088..3caa1f5 100644 --- a/backend/src/main/resources/resort-cameras-local.json +++ b/backend/src/main/resources/resort-cameras-local.json @@ -346,7 +346,7 @@ "code": "CAM_01", "name": "호크 / 스패로우", "sourceType": "HLS", - "sourceUrl": "https://streaming.phoenixhnr.co.kr/hls/yh_02.m3u8", + "sourceUrl": "https://streaming.phoenixhnr.co.kr/hls/yh_02_02.m3u8", "externalPageUrl": "https://phoenixhnr.co.kr/page/pyeongchang/guide/operation/sketchMovie", "displayOrder": 1 }, @@ -355,7 +355,7 @@ "code": "CAM_02", "name": "도도", "sourceType": "HLS", - "sourceUrl": "https://streaming.phoenixhnr.co.kr/hls/sp_01.m3u8", + "sourceUrl": "https://streaming.phoenixhnr.co.kr/hls/sp_01_01.m3u8", "externalPageUrl": "https://phoenixhnr.co.kr/page/pyeongchang/guide/operation/sketchMovie", "displayOrder": 2 }, @@ -364,7 +364,7 @@ "code": "CAM_03", "name": "불새마루", "sourceType": "HLS", - "sourceUrl": "https://streaming.phoenixhnr.co.kr/hls/ht_01.m3u8", + "sourceUrl": "https://streaming.phoenixhnr.co.kr/hls/ht_01_01.m3u8", "externalPageUrl": "https://phoenixhnr.co.kr/page/pyeongchang/guide/operation/sketchMovie", "displayOrder": 3 }, @@ -373,7 +373,7 @@ "code": "CAM_04", "name": "베이스", "sourceType": "HLS", - "sourceUrl": "https://streaming.phoenixhnr.co.kr/hls/bc_02.m3u8", + "sourceUrl": "https://streaming.phoenixhnr.co.kr/hls/bc_02_01.m3u8", "externalPageUrl": "https://phoenixhnr.co.kr/page/pyeongchang/guide/operation/sketchMovie", "displayOrder": 4 }, @@ -391,7 +391,7 @@ "code": "CAM_06", "name": "스노우 빌리지", "sourceType": "HLS", - "sourceUrl": "https://streaming.phoenixhnr.co.kr/hls/yh_01.m3u8", + "sourceUrl": "https://streaming.phoenixhnr.co.kr/hls/yh_01_01.m3u8", "externalPageUrl": "https://phoenixhnr.co.kr/page/pyeongchang/guide/operation/sketchMovie", "displayOrder": 6 }, @@ -400,7 +400,7 @@ "code": "CAM_07", "name": "몽블랑 정상", "sourceType": "HLS", - "sourceUrl": "https://streaming.phoenixhnr.co.kr/hls/mb_03.m3u8", + "sourceUrl": "https://streaming.phoenixhnr.co.kr/hls/mb_03_02.m3u8", "externalPageUrl": "https://phoenixhnr.co.kr/page/pyeongchang/guide/operation/sketchMovie", "displayOrder": 7 }, @@ -463,7 +463,7 @@ "code": "CAM_01", "name": "하이원탑 (제우스1 입구)", "sourceType": "HLS", - "sourceUrl": "/api/high1-stream/1/playlist.m3u8", + "sourceUrl": "/stream-proxy/high1/1/playlist.m3u8", "externalPageUrl": "https://www.high1.com/webcam/pop_webcam.do?ch=1", "displayOrder": 1 }, @@ -472,7 +472,7 @@ "code": "CAM_02", "name": "하이원탑 (헤라2 입구)", "sourceType": "HLS", - "sourceUrl": "/api/high1-stream/2/playlist.m3u8", + "sourceUrl": "/stream-proxy/high1/2/playlist.m3u8", "externalPageUrl": "https://www.high1.com/webcam/pop_webcam.do?ch=2", "displayOrder": 2 }, @@ -481,7 +481,7 @@ "code": "CAM_03", "name": "마운틴허브 (스노우월드 입구)", "sourceType": "HLS", - "sourceUrl": "/api/high1-stream/4/playlist.m3u8", + "sourceUrl": "/stream-proxy/high1/4/playlist.m3u8", "externalPageUrl": "https://www.high1.com/webcam/pop_webcam.do?ch=4", "displayOrder": 3 }, @@ -490,7 +490,7 @@ "code": "CAM_04", "name": "마운틴허브 (아폴로3 입구)", "sourceType": "HLS", - "sourceUrl": "/api/high1-stream/5/playlist.m3u8", + "sourceUrl": "/stream-proxy/high1/5/playlist.m3u8", "externalPageUrl": "https://www.high1.com/webcam/pop_webcam.do?ch=5", "displayOrder": 4 }, @@ -499,7 +499,7 @@ "code": "CAM_05", "name": "마운틴허브 (아테나2 입구)", "sourceType": "HLS", - "sourceUrl": "/api/high1-stream/6/playlist.m3u8", + "sourceUrl": "/stream-proxy/high1/6/playlist.m3u8", "externalPageUrl": "https://www.high1.com/webcam/pop_webcam.do?ch=6", "displayOrder": 5 }, @@ -508,7 +508,7 @@ "code": "CAM_06", "name": "마운틴 베이스 (아테나 리프트)", "sourceType": "HLS", - "sourceUrl": "/api/high1-stream/7/playlist.m3u8", + "sourceUrl": "/stream-proxy/high1/7/playlist.m3u8", "externalPageUrl": "https://www.high1.com/webcam/pop_webcam.do?ch=7", "displayOrder": 6 }, @@ -517,7 +517,7 @@ "code": "CAM_07", "name": "아테나2 하단", "sourceType": "HLS", - "sourceUrl": "/api/high1-stream/8/playlist.m3u8", + "sourceUrl": "/stream-proxy/high1/8/playlist.m3u8", "externalPageUrl": "https://www.high1.com/webcam/pop_webcam.do?ch=8", "displayOrder": 7 }, @@ -526,7 +526,7 @@ "code": "CAM_08", "name": "밸리허브 (헤라 리프트 입구)", "sourceType": "HLS", - "sourceUrl": "/api/high1-stream/11/playlist.m3u8", + "sourceUrl": "/stream-proxy/high1/11/playlist.m3u8", "externalPageUrl": "https://www.high1.com/webcam/pop_webcam.do?ch=11", "displayOrder": 8 }, @@ -535,7 +535,7 @@ "code": "CAM_09", "name": "밸리허브 (아폴로3 합류)", "sourceType": "HLS", - "sourceUrl": "/api/high1-stream/12/playlist.m3u8", + "sourceUrl": "/stream-proxy/high1/12/playlist.m3u8", "externalPageUrl": "https://www.high1.com/webcam/pop_webcam.do?ch=12", "displayOrder": 9 }, @@ -544,7 +544,7 @@ "code": "CAM_10", "name": "밸리허브 (제우스3)", "sourceType": "HLS", - "sourceUrl": "/api/high1-stream/13/playlist.m3u8", + "sourceUrl": "/stream-proxy/high1/13/playlist.m3u8", "externalPageUrl": "https://www.high1.com/webcam/pop_webcam.do?ch=13", "displayOrder": 10 }, @@ -553,7 +553,7 @@ "code": "CAM_11", "name": "제우스3 중단", "sourceType": "HLS", - "sourceUrl": "/api/high1-stream/14/playlist.m3u8", + "sourceUrl": "/stream-proxy/high1/14/playlist.m3u8", "externalPageUrl": "https://www.high1.com/webcam/pop_webcam.do?ch=14", "displayOrder": 11 }, @@ -562,7 +562,7 @@ "code": "CAM_12", "name": "아폴로4 중단", "sourceType": "HLS", - "sourceUrl": "/api/high1-stream/15/playlist.m3u8", + "sourceUrl": "/stream-proxy/high1/15/playlist.m3u8", "externalPageUrl": "https://www.high1.com/webcam/pop_webcam.do?ch=15", "displayOrder": 12 }, @@ -571,7 +571,7 @@ "code": "CAM_13", "name": "아폴로 베이스", "sourceType": "HLS", - "sourceUrl": "/api/high1-stream/16/playlist.m3u8", + "sourceUrl": "/stream-proxy/high1/16/playlist.m3u8", "externalPageUrl": "https://www.high1.com/webcam/pop_webcam.do?ch=16", "displayOrder": 13 }, @@ -580,7 +580,7 @@ "code": "CAM_14", "name": "제우스3 하단", "sourceType": "HLS", - "sourceUrl": "/api/high1-stream/17/playlist.m3u8", + "sourceUrl": "/stream-proxy/high1/17/playlist.m3u8", "externalPageUrl": "https://www.high1.com/webcam/pop_webcam.do?ch=17", "displayOrder": 14 }, @@ -589,7 +589,7 @@ "code": "CAM_15", "name": "밸리 베이스", "sourceType": "HLS", - "sourceUrl": "/api/high1-stream/18/playlist.m3u8", + "sourceUrl": "/stream-proxy/high1/18/playlist.m3u8", "externalPageUrl": "https://www.high1.com/webcam/pop_webcam.do?ch=18", "displayOrder": 15 }, @@ -598,7 +598,7 @@ "code": "CAM_01", "name": "만선하우스", "sourceType": "HLS", - "sourceUrl": "/api/muju-stream/01/playlist.m3u8", + "sourceUrl": "/stream-proxy/muju/01/playlist.m3u8", "externalPageUrl": "http://www.mdysresort.com/guide/webcam_popup_jh.asp?cam_num=01", "displayOrder": 1 }, @@ -607,7 +607,7 @@ "code": "CAM_02", "name": "만선봉 정상", "sourceType": "HLS", - "sourceUrl": "/api/muju-stream/02/playlist.m3u8", + "sourceUrl": "/stream-proxy/muju/02/playlist.m3u8", "externalPageUrl": "http://www.mdysresort.com/guide/webcam_popup_jh.asp?cam_num=02", "displayOrder": 2 }, @@ -616,7 +616,7 @@ "code": "CAM_03", "name": "하이디하우스", "sourceType": "HLS", - "sourceUrl": "/api/muju-stream/03/playlist.m3u8", + "sourceUrl": "/stream-proxy/muju/03/playlist.m3u8", "externalPageUrl": "http://www.mdysresort.com/guide/webcam_popup_jh.asp?cam_num=03", "displayOrder": 3 }, @@ -625,7 +625,7 @@ "code": "CAM_04", "name": "서역기행, 썬다운", "sourceType": "HLS", - "sourceUrl": "/api/muju-stream/04/playlist.m3u8", + "sourceUrl": "/stream-proxy/muju/04/playlist.m3u8", "externalPageUrl": "http://www.mdysresort.com/guide/webcam_popup_jh.asp?cam_num=04", "displayOrder": 4 }, @@ -634,7 +634,7 @@ "code": "CAM_05", "name": "설천하우스", "sourceType": "HLS", - "sourceUrl": "/api/muju-stream/05/playlist.m3u8", + "sourceUrl": "/stream-proxy/muju/05/playlist.m3u8", "externalPageUrl": "http://www.mdysresort.com/guide/webcam_popup_jh.asp?cam_num=05", "displayOrder": 5 }, @@ -643,7 +643,7 @@ "code": "CAM_06", "name": "설천 상단 슬로프", "sourceType": "HLS", - "sourceUrl": "/api/muju-stream/06/playlist.m3u8", + "sourceUrl": "/stream-proxy/muju/06/playlist.m3u8", "externalPageUrl": "http://www.mdysresort.com/guide/webcam_popup_jh.asp?cam_num=06", "displayOrder": 6 }, @@ -652,7 +652,7 @@ "code": "CAM_07", "name": "설천봉 정상", "sourceType": "HLS", - "sourceUrl": "/api/muju-stream/07/playlist.m3u8", + "sourceUrl": "/stream-proxy/muju/07/playlist.m3u8", "externalPageUrl": "http://www.mdysresort.com/guide/webcam_popup_jh.asp?cam_num=07", "displayOrder": 7 }, @@ -661,7 +661,7 @@ "code": "CAM_08", "name": "모차르트, 미뉴에트", "sourceType": "HLS", - "sourceUrl": "/api/muju-stream/08/playlist.m3u8", + "sourceUrl": "/stream-proxy/muju/08/playlist.m3u8", "externalPageUrl": "http://www.mdysresort.com/guide/webcam_popup_jh.asp?cam_num=08", "displayOrder": 8 }, @@ -670,7 +670,7 @@ "code": "CAM_09", "name": "폴카", "sourceType": "HLS", - "sourceUrl": "/api/muju-stream/09/playlist.m3u8", + "sourceUrl": "/stream-proxy/muju/09/playlist.m3u8", "externalPageUrl": "http://www.mdysresort.com/guide/webcam_popup_jh.asp?cam_num=09", "displayOrder": 9 }, @@ -679,7 +679,7 @@ "code": "CAM_10", "name": "실크로드, 미뉴에트 하단", "sourceType": "HLS", - "sourceUrl": "/api/muju-stream/10/playlist.m3u8", + "sourceUrl": "/stream-proxy/muju/10/playlist.m3u8", "externalPageUrl": "http://www.mdysresort.com/guide/webcam_popup_jh.asp?cam_num=10", "displayOrder": 10 }, @@ -688,7 +688,7 @@ "code": "CAM_11", "name": "커넥션", "sourceType": "HLS", - "sourceUrl": "/api/muju-stream/11/playlist.m3u8", + "sourceUrl": "/stream-proxy/muju/11/playlist.m3u8", "externalPageUrl": "http://www.mdysresort.com/guide/webcam_popup_jh.asp?cam_num=11", "displayOrder": 11 }, @@ -715,7 +715,7 @@ "code": "CAM_01", "name": "스키하우스", "sourceType": "HLS", - "sourceUrl": "/api/o2-stream/cam0.m3u8", + "sourceUrl": "/stream-proxy/o2/cam0.m3u8", "externalPageUrl": "https://www.o2resort.com/ski/webcam.asp", "displayOrder": 1 }, @@ -724,7 +724,7 @@ "code": "CAM_02", "name": "오렌지", "sourceType": "HLS", - "sourceUrl": "/api/o2-stream/cam1.m3u8", + "sourceUrl": "/stream-proxy/o2/cam1.m3u8", "externalPageUrl": "https://www.o2resort.com/ski/webcam.asp", "displayOrder": 2 }, @@ -733,7 +733,7 @@ "code": "CAM_03", "name": "버금마루", "sourceType": "HLS", - "sourceUrl": "/api/o2-stream/cam2.m3u8", + "sourceUrl": "/stream-proxy/o2/cam2.m3u8", "externalPageUrl": "https://www.o2resort.com/ski/webcam.asp", "displayOrder": 3 }, @@ -742,7 +742,7 @@ "code": "CAM_04", "name": "으뜸마루", "sourceType": "HLS", - "sourceUrl": "/api/o2-stream/cam3.m3u8", + "sourceUrl": "/stream-proxy/o2/cam3.m3u8", "externalPageUrl": "https://www.o2resort.com/ski/webcam.asp", "displayOrder": 4 }, @@ -751,7 +751,7 @@ "code": "CAM_05", "name": "글로리2 상단", "sourceType": "HLS", - "sourceUrl": "/api/o2-stream/cam20.m3u8", + "sourceUrl": "/stream-proxy/o2/cam20.m3u8", "externalPageUrl": "https://www.o2resort.com/ski/webcam.asp", "displayOrder": 5 }, @@ -760,7 +760,7 @@ "code": "CAM_06", "name": "글로리3 상단", "sourceType": "HLS", - "sourceUrl": "/api/o2-stream/cam21.m3u8", + "sourceUrl": "/stream-proxy/o2/cam21.m3u8", "externalPageUrl": "https://www.o2resort.com/ski/webcam.asp", "displayOrder": 6 }, @@ -769,7 +769,7 @@ "code": "CAM_07", "name": "드림2 상단", "sourceType": "HLS", - "sourceUrl": "/api/o2-stream/cam22.m3u8", + "sourceUrl": "/stream-proxy/o2/cam22.m3u8", "externalPageUrl": "https://www.o2resort.com/ski/webcam.asp", "displayOrder": 7 }, @@ -778,7 +778,7 @@ "code": "CAM_08", "name": "글로리3", "sourceType": "HLS", - "sourceUrl": "/api/o2-stream/cam23.m3u8", + "sourceUrl": "/stream-proxy/o2/cam23.m3u8", "externalPageUrl": "https://www.o2resort.com/ski/webcam.asp", "displayOrder": 8 } diff --git a/database/production/006_migration_fix_stream_proxy_path.sql b/database/production/006_migration_fix_stream_proxy_path.sql new file mode 100644 index 0000000..edfd101 --- /dev/null +++ b/database/production/006_migration_fix_stream_proxy_path.sql @@ -0,0 +1,58 @@ +-- ==================================================================== +-- Snowthing Production Migration +-- Version: 006 +-- Description: 휘닉스파크 HLS 스트림 최신화 및 프론트엔드 스트림 프록시 경로(/stream-proxy/) 전환 +-- ==================================================================== + +-- 1. 휘닉스파크 6개소 최신 실시간 HLS 스트림 URL 갱신 +UPDATE `resort_camera` rc +JOIN `resort` r ON rc.resort_id = r.resort_id +SET rc.source_url = 'https://streaming.phoenixhnr.co.kr/hls/yh_02_02.m3u8' +WHERE r.code = 'PHOENIX' AND rc.code = 'CAM_01'; + +UPDATE `resort_camera` rc +JOIN `resort` r ON rc.resort_id = r.resort_id +SET rc.source_url = 'https://streaming.phoenixhnr.co.kr/hls/sp_01_01.m3u8' +WHERE r.code = 'PHOENIX' AND rc.code = 'CAM_02'; + +UPDATE `resort_camera` rc +JOIN `resort` r ON rc.resort_id = r.resort_id +SET rc.source_url = 'https://streaming.phoenixhnr.co.kr/hls/ht_01_01.m3u8' +WHERE r.code = 'PHOENIX' AND rc.code = 'CAM_03'; + +UPDATE `resort_camera` rc +JOIN `resort` r ON rc.resort_id = r.resort_id +SET rc.source_url = 'https://streaming.phoenixhnr.co.kr/hls/bc_02_01.m3u8' +WHERE r.code = 'PHOENIX' AND rc.code = 'CAM_04'; + +UPDATE `resort_camera` rc +JOIN `resort` r ON rc.resort_id = r.resort_id +SET rc.source_url = 'https://streaming.phoenixhnr.co.kr/hls/yh_01_01.m3u8' +WHERE r.code = 'PHOENIX' AND rc.code = 'CAM_06'; + +UPDATE `resort_camera` rc +JOIN `resort` r ON rc.resort_id = r.resort_id +SET rc.source_url = 'https://streaming.phoenixhnr.co.kr/hls/mb_03_02.m3u8' +WHERE r.code = 'PHOENIX' AND rc.code = 'CAM_07'; + +-- 2. 하이원, 오투, 무주 스트림 프록시 경로 변경 (/api/* -> /stream-proxy/*) +UPDATE `resort_camera` rc +JOIN `resort` r ON rc.resort_id = r.resort_id +SET rc.source_url = REPLACE(rc.source_url, '/api/high1-stream/', '/stream-proxy/high1/') +WHERE r.code = 'HIGH1'; + +UPDATE `resort_camera` rc +JOIN `resort` r ON rc.resort_id = r.resort_id +SET rc.source_url = REPLACE(rc.source_url, '/api/o2-stream/', '/stream-proxy/o2/') +WHERE r.code = 'O2_RESORT'; + +UPDATE `resort_camera` rc +JOIN `resort` r ON rc.resort_id = r.resort_id +SET rc.source_url = REPLACE(rc.source_url, '/api/muju-stream/', '/stream-proxy/muju/') +WHERE r.code = 'MUJU'; + +-- 3. 비발디파크 CAM_09 (클래식) 정품 시리얼 번호 보정 +UPDATE `resort_camera` rc +JOIN `resort` r ON rc.resort_id = r.resort_id +SET rc.source_url = '/vivaldi.html?channel=10&serial=TW0014A15451&autoplay=true' +WHERE r.code = 'VIVALDI' AND rc.code = 'CAM_09'; diff --git a/docs/project/work.md b/docs/project/work.md index a1c7a4d..426f3b8 100644 --- a/docs/project/work.md +++ b/docs/project/work.md @@ -1,3 +1,20 @@ +- **슬로프캠 운영 환경 스트림 미출력 이슈 전수 조치 (2026-10-01)**: + - 상태: 구현, 빌드 검증, 마이그레이션 006 작성 완료 + - 내용: + - **비발디파크 404 해결**: `frontend/Dockerfile`의 runner 스테이지에 `COPY --from=builder /app/public ./public` 추가하여 운영 컨테이너에 `vivaldi.html` 및 `vivaldi.js` 정적 에셋이 포함되도록 수정. + - **하이원·오투·무주 401 해결**: Nginx의 `/api/` ➔ 백엔드(8080) 라우팅 규칙과의 충돌을 피하기 위해 Next.js 스트림 프록시 라우트를 `/api/*-stream`에서 `/stream-proxy/*`(`high1`, `o2`, `muju`)로 분리 이동. + - **휘닉스파크 404 해결 및 006 마이그레이션**: + - `database/production/006_migration_fix_stream_proxy_path.sql` 신설: + - 휘닉스파크 6개소(호크, 도도, 불새마루, 베이스, 스노우빌리지, 몽블랑) `source_url`을 200 OK로 실제 송출 중인 최신 HLS 주소로 업데이트. + - 하이원, 오투, 무주의 `source_url`을 `/stream-proxy/...` 경로로 일괄 치환(`REPLACE`). + - 비발디 `CAM_09`(클래식) 정품 시리얼(`TW0014A15451`) 보정 보장. + - `scripts/verify-production-migration-006.sh` 작성 및 CI 워크플로우(`gradle.yml`, `deploy-backend.yml`) 연동. + - **프론트엔드 및 로컬 시드 정합성 동기화**: + - `fallback-data.ts` 및 `resort-cameras-local.json`의 스트림 URL을 `/stream-proxy/...` 및 최신 휘닉스파크 주소로 전면 동기화. + - **빌드 검증**: + - 프론트엔드 Turbopack 프로덕션 빌드(`npm run build`) 통과. + - 백엔드 컴파일 및 `spotlessCheck` 통과. + - **슬로프캠 백엔드 CI 통과 및 CodeRabbit 피드백 전수 조치 (2026-09-29)**: - 상태: 구현, 테스트 통과, DB/시드/CI스크립트 동기화 완료 - 내용: diff --git a/frontend/Dockerfile b/frontend/Dockerfile index 816abc4..82ffa5e 100644 --- a/frontend/Dockerfile +++ b/frontend/Dockerfile @@ -35,6 +35,7 @@ COPY --from=builder /app/package.json ./package.json COPY --from=builder /app/package-lock.json ./package-lock.json COPY --from=builder /app/node_modules ./node_modules COPY --from=builder /app/.next ./.next +COPY --from=builder /app/public ./public USER nextjs diff --git a/frontend/app/resort-cam/fallback-data.ts b/frontend/app/resort-cam/fallback-data.ts index 2890846..730db67 100644 --- a/frontend/app/resort-cam/fallback-data.ts +++ b/frontend/app/resort-cam/fallback-data.ts @@ -331,7 +331,7 @@ export const FALLBACK_RESORTS: ResortCameraGroup[] = [ code: "CAM_01", name: "하이원탑 (제우스1 입구)", sourceType: "HLS", - sourceUrl: "/api/high1-stream/1/playlist.m3u8", + sourceUrl: "/stream-proxy/high1/1/playlist.m3u8", externalPageUrl: "https://www.high1.com/webcam/pop_webcam.do?ch=1", displayOrder: 1, }, @@ -339,7 +339,7 @@ export const FALLBACK_RESORTS: ResortCameraGroup[] = [ code: "CAM_02", name: "하이원탑 (헤라2 입구)", sourceType: "HLS", - sourceUrl: "/api/high1-stream/2/playlist.m3u8", + sourceUrl: "/stream-proxy/high1/2/playlist.m3u8", externalPageUrl: "https://www.high1.com/webcam/pop_webcam.do?ch=2", displayOrder: 2, }, @@ -347,7 +347,7 @@ export const FALLBACK_RESORTS: ResortCameraGroup[] = [ code: "CAM_03", name: "마운틴허브 (스노우월드 입구)", sourceType: "HLS", - sourceUrl: "/api/high1-stream/4/playlist.m3u8", + sourceUrl: "/stream-proxy/high1/4/playlist.m3u8", externalPageUrl: "https://www.high1.com/webcam/pop_webcam.do?ch=4", displayOrder: 3, }, @@ -355,7 +355,7 @@ export const FALLBACK_RESORTS: ResortCameraGroup[] = [ code: "CAM_04", name: "마운틴허브 (아폴로3 입구)", sourceType: "HLS", - sourceUrl: "/api/high1-stream/5/playlist.m3u8", + sourceUrl: "/stream-proxy/high1/5/playlist.m3u8", externalPageUrl: "https://www.high1.com/webcam/pop_webcam.do?ch=5", displayOrder: 4, }, @@ -363,7 +363,7 @@ export const FALLBACK_RESORTS: ResortCameraGroup[] = [ code: "CAM_05", name: "마운틴허브 (아테나2 입구)", sourceType: "HLS", - sourceUrl: "/api/high1-stream/6/playlist.m3u8", + sourceUrl: "/stream-proxy/high1/6/playlist.m3u8", externalPageUrl: "https://www.high1.com/webcam/pop_webcam.do?ch=6", displayOrder: 5, }, @@ -371,7 +371,7 @@ export const FALLBACK_RESORTS: ResortCameraGroup[] = [ code: "CAM_06", name: "마운틴 베이스 (아테나 리프트)", sourceType: "HLS", - sourceUrl: "/api/high1-stream/7/playlist.m3u8", + sourceUrl: "/stream-proxy/high1/7/playlist.m3u8", externalPageUrl: "https://www.high1.com/webcam/pop_webcam.do?ch=7", displayOrder: 6, }, @@ -379,7 +379,7 @@ export const FALLBACK_RESORTS: ResortCameraGroup[] = [ code: "CAM_07", name: "아테나2 하단", sourceType: "HLS", - sourceUrl: "/api/high1-stream/8/playlist.m3u8", + sourceUrl: "/stream-proxy/high1/8/playlist.m3u8", externalPageUrl: "https://www.high1.com/webcam/pop_webcam.do?ch=8", displayOrder: 7, }, @@ -387,7 +387,7 @@ export const FALLBACK_RESORTS: ResortCameraGroup[] = [ code: "CAM_08", name: "밸리허브 (헤라 리프트 입구)", sourceType: "HLS", - sourceUrl: "/api/high1-stream/11/playlist.m3u8", + sourceUrl: "/stream-proxy/high1/11/playlist.m3u8", externalPageUrl: "https://www.high1.com/webcam/pop_webcam.do?ch=11", displayOrder: 8, }, @@ -395,7 +395,7 @@ export const FALLBACK_RESORTS: ResortCameraGroup[] = [ code: "CAM_09", name: "밸리허브 (아폴로3 합류)", sourceType: "HLS", - sourceUrl: "/api/high1-stream/12/playlist.m3u8", + sourceUrl: "/stream-proxy/high1/12/playlist.m3u8", externalPageUrl: "https://www.high1.com/webcam/pop_webcam.do?ch=12", displayOrder: 9, }, @@ -403,7 +403,7 @@ export const FALLBACK_RESORTS: ResortCameraGroup[] = [ code: "CAM_10", name: "밸리허브 (제우스3)", sourceType: "HLS", - sourceUrl: "/api/high1-stream/13/playlist.m3u8", + sourceUrl: "/stream-proxy/high1/13/playlist.m3u8", externalPageUrl: "https://www.high1.com/webcam/pop_webcam.do?ch=13", displayOrder: 10, }, @@ -411,7 +411,7 @@ export const FALLBACK_RESORTS: ResortCameraGroup[] = [ code: "CAM_11", name: "제우스3 중단", sourceType: "HLS", - sourceUrl: "/api/high1-stream/14/playlist.m3u8", + sourceUrl: "/stream-proxy/high1/14/playlist.m3u8", externalPageUrl: "https://www.high1.com/webcam/pop_webcam.do?ch=14", displayOrder: 11, }, @@ -419,7 +419,7 @@ export const FALLBACK_RESORTS: ResortCameraGroup[] = [ code: "CAM_12", name: "아폴로4 중단", sourceType: "HLS", - sourceUrl: "/api/high1-stream/15/playlist.m3u8", + sourceUrl: "/stream-proxy/high1/15/playlist.m3u8", externalPageUrl: "https://www.high1.com/webcam/pop_webcam.do?ch=15", displayOrder: 12, }, @@ -427,7 +427,7 @@ export const FALLBACK_RESORTS: ResortCameraGroup[] = [ code: "CAM_13", name: "아폴로 베이스", sourceType: "HLS", - sourceUrl: "/api/high1-stream/16/playlist.m3u8", + sourceUrl: "/stream-proxy/high1/16/playlist.m3u8", externalPageUrl: "https://www.high1.com/webcam/pop_webcam.do?ch=16", displayOrder: 13, }, @@ -435,7 +435,7 @@ export const FALLBACK_RESORTS: ResortCameraGroup[] = [ code: "CAM_14", name: "제우스3 하단", sourceType: "HLS", - sourceUrl: "/api/high1-stream/17/playlist.m3u8", + sourceUrl: "/stream-proxy/high1/17/playlist.m3u8", externalPageUrl: "https://www.high1.com/webcam/pop_webcam.do?ch=17", displayOrder: 14, }, @@ -443,7 +443,7 @@ export const FALLBACK_RESORTS: ResortCameraGroup[] = [ code: "CAM_15", name: "밸리 베이스", sourceType: "HLS", - sourceUrl: "/api/high1-stream/18/playlist.m3u8", + sourceUrl: "/stream-proxy/high1/18/playlist.m3u8", externalPageUrl: "https://www.high1.com/webcam/pop_webcam.do?ch=18", displayOrder: 15, }, @@ -643,7 +643,7 @@ export const FALLBACK_RESORTS: ResortCameraGroup[] = [ code: "CAM_01", name: "만선하우스", sourceType: "HLS", - sourceUrl: "/api/muju-stream/01/playlist.m3u8", + sourceUrl: "/stream-proxy/muju/01/playlist.m3u8", externalPageUrl: "http://www.mdysresort.com/guide/webcam_popup_jh.asp?cam_num=01", displayOrder: 1, }, @@ -651,7 +651,7 @@ export const FALLBACK_RESORTS: ResortCameraGroup[] = [ code: "CAM_02", name: "만선봉 정상", sourceType: "HLS", - sourceUrl: "/api/muju-stream/02/playlist.m3u8", + sourceUrl: "/stream-proxy/muju/02/playlist.m3u8", externalPageUrl: "http://www.mdysresort.com/guide/webcam_popup_jh.asp?cam_num=02", displayOrder: 2, }, @@ -659,7 +659,7 @@ export const FALLBACK_RESORTS: ResortCameraGroup[] = [ code: "CAM_03", name: "하이디하우스", sourceType: "HLS", - sourceUrl: "/api/muju-stream/03/playlist.m3u8", + sourceUrl: "/stream-proxy/muju/03/playlist.m3u8", externalPageUrl: "http://www.mdysresort.com/guide/webcam_popup_jh.asp?cam_num=03", displayOrder: 3, }, @@ -667,7 +667,7 @@ export const FALLBACK_RESORTS: ResortCameraGroup[] = [ code: "CAM_04", name: "서역기행, 썬다운", sourceType: "HLS", - sourceUrl: "/api/muju-stream/04/playlist.m3u8", + sourceUrl: "/stream-proxy/muju/04/playlist.m3u8", externalPageUrl: "http://www.mdysresort.com/guide/webcam_popup_jh.asp?cam_num=04", displayOrder: 4, }, @@ -675,7 +675,7 @@ export const FALLBACK_RESORTS: ResortCameraGroup[] = [ code: "CAM_05", name: "설천하우스", sourceType: "HLS", - sourceUrl: "/api/muju-stream/05/playlist.m3u8", + sourceUrl: "/stream-proxy/muju/05/playlist.m3u8", externalPageUrl: "http://www.mdysresort.com/guide/webcam_popup_jh.asp?cam_num=05", displayOrder: 5, }, @@ -683,7 +683,7 @@ export const FALLBACK_RESORTS: ResortCameraGroup[] = [ code: "CAM_06", name: "설천 상단 슬로프", sourceType: "HLS", - sourceUrl: "/api/muju-stream/06/playlist.m3u8", + sourceUrl: "/stream-proxy/muju/06/playlist.m3u8", externalPageUrl: "http://www.mdysresort.com/guide/webcam_popup_jh.asp?cam_num=06", displayOrder: 6, }, @@ -691,7 +691,7 @@ export const FALLBACK_RESORTS: ResortCameraGroup[] = [ code: "CAM_07", name: "설천봉 정상", sourceType: "HLS", - sourceUrl: "/api/muju-stream/07/playlist.m3u8", + sourceUrl: "/stream-proxy/muju/07/playlist.m3u8", externalPageUrl: "http://www.mdysresort.com/guide/webcam_popup_jh.asp?cam_num=07", displayOrder: 7, }, @@ -699,7 +699,7 @@ export const FALLBACK_RESORTS: ResortCameraGroup[] = [ code: "CAM_08", name: "모차르트, 미뉴에트", sourceType: "HLS", - sourceUrl: "/api/muju-stream/08/playlist.m3u8", + sourceUrl: "/stream-proxy/muju/08/playlist.m3u8", externalPageUrl: "http://www.mdysresort.com/guide/webcam_popup_jh.asp?cam_num=08", displayOrder: 8, }, @@ -707,7 +707,7 @@ export const FALLBACK_RESORTS: ResortCameraGroup[] = [ code: "CAM_09", name: "폴카", sourceType: "HLS", - sourceUrl: "/api/muju-stream/09/playlist.m3u8", + sourceUrl: "/stream-proxy/muju/09/playlist.m3u8", externalPageUrl: "http://www.mdysresort.com/guide/webcam_popup_jh.asp?cam_num=09", displayOrder: 9, }, @@ -715,7 +715,7 @@ export const FALLBACK_RESORTS: ResortCameraGroup[] = [ code: "CAM_10", name: "실크로드, 미뉴에트 하단", sourceType: "HLS", - sourceUrl: "/api/muju-stream/10/playlist.m3u8", + sourceUrl: "/stream-proxy/muju/10/playlist.m3u8", externalPageUrl: "http://www.mdysresort.com/guide/webcam_popup_jh.asp?cam_num=10", displayOrder: 10, }, @@ -723,7 +723,7 @@ export const FALLBACK_RESORTS: ResortCameraGroup[] = [ code: "CAM_11", name: "커넥션", sourceType: "HLS", - sourceUrl: "/api/muju-stream/11/playlist.m3u8", + sourceUrl: "/stream-proxy/muju/11/playlist.m3u8", externalPageUrl: "http://www.mdysresort.com/guide/webcam_popup_jh.asp?cam_num=11", displayOrder: 11, }, @@ -739,7 +739,7 @@ export const FALLBACK_RESORTS: ResortCameraGroup[] = [ code: "CAM_01", name: "스키하우스", sourceType: "HLS", - sourceUrl: "/api/o2-stream/cam0.m3u8", + sourceUrl: "/stream-proxy/o2/cam0.m3u8", externalPageUrl: "https://www.o2resort.com/ski/webcam.asp", displayOrder: 1, }, @@ -747,7 +747,7 @@ export const FALLBACK_RESORTS: ResortCameraGroup[] = [ code: "CAM_02", name: "오렌지", sourceType: "HLS", - sourceUrl: "/api/o2-stream/cam1.m3u8", + sourceUrl: "/stream-proxy/o2/cam1.m3u8", externalPageUrl: "https://www.o2resort.com/ski/webcam.asp", displayOrder: 2, }, @@ -755,7 +755,7 @@ export const FALLBACK_RESORTS: ResortCameraGroup[] = [ code: "CAM_03", name: "버금마루", sourceType: "HLS", - sourceUrl: "/api/o2-stream/cam2.m3u8", + sourceUrl: "/stream-proxy/o2/cam2.m3u8", externalPageUrl: "https://www.o2resort.com/ski/webcam.asp", displayOrder: 3, }, @@ -763,7 +763,7 @@ export const FALLBACK_RESORTS: ResortCameraGroup[] = [ code: "CAM_04", name: "으뜸마루", sourceType: "HLS", - sourceUrl: "/api/o2-stream/cam3.m3u8", + sourceUrl: "/stream-proxy/o2/cam3.m3u8", externalPageUrl: "https://www.o2resort.com/ski/webcam.asp", displayOrder: 4, }, @@ -771,7 +771,7 @@ export const FALLBACK_RESORTS: ResortCameraGroup[] = [ code: "CAM_05", name: "글로리2 상단", sourceType: "HLS", - sourceUrl: "/api/o2-stream/cam20.m3u8", + sourceUrl: "/stream-proxy/o2/cam20.m3u8", externalPageUrl: "https://www.o2resort.com/ski/webcam.asp", displayOrder: 5, }, @@ -779,7 +779,7 @@ export const FALLBACK_RESORTS: ResortCameraGroup[] = [ code: "CAM_06", name: "글로리3 상단", sourceType: "HLS", - sourceUrl: "/api/o2-stream/cam21.m3u8", + sourceUrl: "/stream-proxy/o2/cam21.m3u8", externalPageUrl: "https://www.o2resort.com/ski/webcam.asp", displayOrder: 6, }, @@ -787,7 +787,7 @@ export const FALLBACK_RESORTS: ResortCameraGroup[] = [ code: "CAM_07", name: "드림2 상단", sourceType: "HLS", - sourceUrl: "/api/o2-stream/cam22.m3u8", + sourceUrl: "/stream-proxy/o2/cam22.m3u8", externalPageUrl: "https://www.o2resort.com/ski/webcam.asp", displayOrder: 7, }, @@ -795,7 +795,7 @@ export const FALLBACK_RESORTS: ResortCameraGroup[] = [ code: "CAM_08", name: "글로리3", sourceType: "HLS", - sourceUrl: "/api/o2-stream/cam23.m3u8", + sourceUrl: "/stream-proxy/o2/cam23.m3u8", externalPageUrl: "https://www.o2resort.com/ski/webcam.asp", displayOrder: 8, }, diff --git a/frontend/app/api/high1-stream/[ch]/[file]/route.ts b/frontend/app/stream-proxy/high1/[ch]/[file]/route.ts similarity index 100% rename from frontend/app/api/high1-stream/[ch]/[file]/route.ts rename to frontend/app/stream-proxy/high1/[ch]/[file]/route.ts diff --git a/frontend/app/api/muju-stream/[cam]/[file]/route.ts b/frontend/app/stream-proxy/muju/[cam]/[file]/route.ts similarity index 100% rename from frontend/app/api/muju-stream/[cam]/[file]/route.ts rename to frontend/app/stream-proxy/muju/[cam]/[file]/route.ts diff --git a/frontend/app/api/o2-stream/[file]/route.ts b/frontend/app/stream-proxy/o2/[file]/route.ts similarity index 100% rename from frontend/app/api/o2-stream/[file]/route.ts rename to frontend/app/stream-proxy/o2/[file]/route.ts diff --git a/scripts/verify-production-migration-006.sh b/scripts/verify-production-migration-006.sh new file mode 100644 index 0000000..a12a029 --- /dev/null +++ b/scripts/verify-production-migration-006.sh @@ -0,0 +1,41 @@ +#!/usr/bin/env bash +set -euo pipefail + +DB_HOST="${SNOWTHING_TEST_DB_HOST:-127.0.0.1}" +DB_PORT="${SNOWTHING_TEST_DB_PORT:-3306}" +DB_NAME="${SNOWTHING_TEST_DB_NAME:-snowthing_test}" +DB_USERNAME="${SNOWTHING_TEST_DB_USERNAME:?SNOWTHING_TEST_DB_USERNAME is required}" +DB_PASSWORD="${SNOWTHING_TEST_DB_PASSWORD:?SNOWTHING_TEST_DB_PASSWORD is required}" + +mysql_test() { + MYSQL_PWD="$DB_PASSWORD" mysql \ + --protocol=TCP --host="$DB_HOST" --port="$DB_PORT" \ + --user="$DB_USERNAME" --database="$DB_NAME" \ + --batch --skip-column-names "$@" +} + +mysql_test < database/production/001_initial_schema.sql +mysql_test < database/production/002_reference_data.sql + +run_migrations() { + SNOWTHING_MIGRATION_DB_HOST="$DB_HOST" \ + SNOWTHING_MIGRATION_DB_PORT="$DB_PORT" \ + SNOWTHING_MIGRATION_DB_NAME="$DB_NAME" \ + SNOWTHING_MIGRATION_DB_USERNAME="$DB_USERNAME" \ + SNOWTHING_MIGRATION_DB_PASSWORD="$DB_PASSWORD" \ + SNOWTHING_MIGRATION_DB_AUTH_MODE=password \ + SNOWTHING_MIGRATION_DB_SSL_MODE=DISABLED \ + scripts/run-production-migrations.sh database/production +} + +run_migrations +second_run_output="$(run_migrations)" +grep -q 'Migration already applied: 006_migration_fix_stream_proxy_path' <<< "$second_run_output" + +[[ "$(mysql_test --execute="SELECT COUNT(*) FROM schema_migration WHERE version = '006_migration_fix_stream_proxy_path'")" == "1" ]] +[[ "$(mysql_test --execute="SELECT COUNT(*) FROM resort_camera WHERE source_url LIKE '/api/%-stream/%'")" == "0" ]] +[[ "$(mysql_test --execute="SELECT COUNT(*) FROM resort_camera WHERE source_url LIKE '/stream-proxy/%'")" == "34" ]] +[[ "$(mysql_test --execute="SELECT COUNT(*) FROM resort_camera rc JOIN resort r ON rc.resort_id = r.resort_id WHERE r.code = 'PHOENIX' AND rc.source_url = 'https://streaming.phoenixhnr.co.kr/hls/yh_02_02.m3u8'")" == "1" ]] +[[ "$(mysql_test --execute="SELECT COUNT(*) FROM resort_camera rc JOIN resort r ON rc.resort_id = r.resort_id WHERE r.code = 'VIVALDI' AND rc.code = 'CAM_09' AND rc.source_url LIKE '%TW0014A15451%'")" == "1" ]] + +echo "Production migration 006 verification passed."