From 4040e6621f55259795fba8906c1cc8873eebe44a Mon Sep 17 00:00:00 2001 From: manNomi Date: Tue, 18 Aug 2026 21:38:21 +0900 Subject: [PATCH 1/5] =?UTF-8?q?fix:=20API=20=EB=B0=8F=20ML=20=EC=9A=B4?= =?UTF-8?q?=EC=98=81=20=EC=A3=BC=EC=86=8C=20=EC=A0=84=ED=99=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 18 ++-- .../deployment/IOS_PHYSICAL_DEVICE_TESTING.md | 26 +++--- docs/deployment/SOUNDLOG_SHOP_DOMAIN.md | 62 +++++-------- eas.json | 12 +-- scripts/check-api-origin.js | 89 +++++++++++++++++-- scripts/check-store-release.js | 12 +-- scripts/check-vercel-config.js | 4 +- scripts/require-vercel-api-origin.js | 3 +- 8 files changed, 141 insertions(+), 85 deletions(-) diff --git a/README.md b/README.md index 7a14035..4488132 100644 --- a/README.md +++ b/README.md @@ -41,23 +41,23 @@ EXPO_PUBLIC_MOCK_API_FAIL_ENDPOINTS=playlist.detail npm run web 실제 서버와 연동할 때는 Expo 환경변수로 API base URL을 지정합니다. 이 값이 없으면 mock으로 돌아가지 않고 API URL 누락 오류를 표시합니다. ```bash -EXPO_PUBLIC_SOUNDLOG_API_SOURCE=server EXPO_PUBLIC_SOUNDLOG_API_BASE_URL=https://api.soundlog.shop npm run ios +EXPO_PUBLIC_SOUNDLOG_API_SOURCE=server EXPO_PUBLIC_SOUNDLOG_API_BASE_URL=https://api.soundlog.p-e.kr npm run ios ``` -Soundlog는 웹 서비스를 배포하지 않습니다. 운영 배포 대상은 Expo/EAS로 빌드한 iOS·Android 네이티브 앱이며, 앱은 Vercel 프록시 없이 GCP의 `https://api.soundlog.shop`을 직접 호출합니다. 저장소의 web·Vercel 관련 설정은 로컬 호환성 확인과 과거 실험용 자료일 뿐 운영 배포 경로에 포함하지 않습니다. +Soundlog는 웹 서비스를 배포하지 않습니다. 운영 배포 대상은 Expo/EAS로 빌드한 iOS·Android 네이티브 앱이며, 앱은 Vercel 프록시 없이 API와 ML이 함께 배포된 `https://api.soundlog.p-e.kr`을 직접 호출합니다. 저장소의 web·Vercel 관련 설정은 로컬 호환성 확인과 과거 실험용 자료일 뿐 운영 배포 경로에 포함하지 않습니다. -네이티브 실배포 빌드는 `api.soundlog.shop` 서브도메인을 직접 호출합니다(GCP VM 위 Caddy가 HTTPS를 종료). 로그인은 Soundlog 자체 이메일/비밀번호 계정으로 처리합니다. +네이티브 실배포 빌드는 `api.soundlog.p-e.kr` 서브도메인을 직접 호출합니다. 로그인은 Soundlog 자체 이메일/비밀번호 계정으로 처리합니다. ## 테스트 설치 빌드 -`development`, `preview` EAS profile은 `api.soundlog.shop`을 직접 바라보도록 설정되어 있습니다. +`development`, `preview` EAS profile은 `api.soundlog.p-e.kr`을 직접 바라보도록 설정되어 있습니다. 본인 iPhone을 USB로 연결해 최신 코드를 직접 테스트할 때는 [iPhone 실기기 테스트 가이드](docs/deployment/IOS_PHYSICAL_DEVICE_TESTING.md)를 먼저 확인합니다. 실기기에서 `127.0.0.1`은 Mac이 아니라 iPhone 자신이므로, 로컬 서버를 사용할 때는 Mac의 LAN IP를 앱 빌드 환경변수로 지정해야 합니다. -- API: `https://api.soundlog.shop` +- API 및 ML: `https://api.soundlog.p-e.kr` - 개인정보 처리방침: `https://api.soundlog.shop/legal/privacy` - 서비스 이용약관: `https://api.soundlog.shop/legal/terms` -- 고객지원: `https://api.soundlog.shop/support` +- 고객지원: `support@soundlog.shop` - API source: `server` - auth: Soundlog 자체 이메일/비밀번호 로그인 - iOS/Android: HTTPS API만 사용 @@ -67,12 +67,12 @@ Mock API로 되돌리는 런타임 경로는 제거했습니다. 화면 상태 API origin이 최신 서버인지 확인하려면 아래처럼 실행합니다. ```bash -SOUNDLOG_API_ORIGIN=https://api.soundlog.shop npm run check:api-origin +SOUNDLOG_API_ORIGIN=https://api.soundlog.p-e.kr npm run check:api-origin ``` 이 검사는 로그인 필수 API도 함께 확인하므로 `SOUNDLOG_CHECK_EMAIL`, `SOUNDLOG_CHECK_PASSWORD`를 지정하면 해당 smoke 계정으로 로그인합니다. 값을 -지정하지 않으면 `@soundlog.test` 임시 계정을 생성해 검증합니다. +지정하지 않으면 `@soundlog.test` 임시 계정을 생성해 API와 ML 추천을 검증한 뒤 즉시 삭제합니다. Android 지인 테스트용 내부 배포 빌드는 아래 명령으로 생성합니다. @@ -80,7 +80,7 @@ Android 지인 테스트용 내부 배포 빌드는 아래 명령으로 생성 npx eas build --profile preview --platform android ``` -iOS는 TestFlight 또는 ad hoc 기기 등록이 필요합니다. App Store/TestFlight에 올릴 production profile도 현재는 `https://api.soundlog.shop`을 사용합니다. +iOS는 TestFlight 또는 ad hoc 기기 등록이 필요합니다. App Store/TestFlight에 올릴 production profile도 현재는 `https://api.soundlog.p-e.kr`을 사용합니다. 개인정보 처리방침과 이용약관은 EAS 설정에서 기존 `api.soundlog.shop` 주소를 유지하지만 현재 기존 도메인은 DNS가 해석되지 않고 새 도메인의 공개 문서 경로도 404입니다. 심사 전에는 새 서버에 공개 문서를 배포하거나 기존 도메인의 DNS를 복구해야 합니다. ## 문서 diff --git a/docs/deployment/IOS_PHYSICAL_DEVICE_TESTING.md b/docs/deployment/IOS_PHYSICAL_DEVICE_TESTING.md index 7fd2dd8..341d763 100644 --- a/docs/deployment/IOS_PHYSICAL_DEVICE_TESTING.md +++ b/docs/deployment/IOS_PHYSICAL_DEVICE_TESTING.md @@ -67,7 +67,7 @@ xcrun devicectl list devices ### `127.0.0.1`을 사용하면 안 되는 이유 -iPhone에서 `127.0.0.1`은 Mac이 아니라 **iPhone 자신**입니다. `.env.local`의 아래 값은 시뮬레이터용이며 실기기에서는 서버에 도달하지 못합니다. +iPhone에서 `127.0.0.1`은 Mac이 아니라 **iPhone 자신**입니다. 로컬 서버를 직접 테스트하려고 아래 값을 임시로 사용하면 실기기에서는 서버에 도달하지 못합니다. ```env EXPO_PUBLIC_SOUNDLOG_API_BASE_URL=http://127.0.0.1:4000 @@ -85,6 +85,8 @@ ipconfig getifaddr en0 http://172.30.1.99:4000 ``` +Expo SDK 56 개발 실행에서는 `.env.local`이 셸에 입력한 같은 이름의 환경변수보다 우선합니다. 로컬 서버를 테스트할 때는 `.env.local`의 API와 업로드 주소를 Mac의 LAN 주소로 임시 변경해야 합니다. 테스트가 끝나면 두 값을 `https://api.soundlog.p-e.kr`로 복구합니다. + ### 서버 실행 및 확인 DB와 서버 환경변수를 준비한 뒤 API를 실행합니다. @@ -109,13 +111,17 @@ iPhone Safari에서도 같은 헬스 체크 URL을 열 수 있어야 합니다. ## 5. iPhone에 앱 설치 -앱 디렉터리에서 Mac의 LAN IP를 주입해 개발 빌드를 설치합니다. +앱 디렉터리의 `.env.local`에 Mac의 LAN IP를 임시로 설정합니다. + +```dotenv +EXPO_PUBLIC_SOUNDLOG_API_BASE_URL=http://:4000 +EXPO_PUBLIC_SOUNDLOG_UPLOAD_ORIGIN=http://:4000 +``` + +그다음 개발 빌드를 설치합니다. ```bash cd /Users/manwook-han/Desktop/hmw/code/apps/soundlog/soundlog - -EXPO_PUBLIC_SOUNDLOG_API_SOURCE=server \ -EXPO_PUBLIC_SOUNDLOG_API_BASE_URL=http://:4000 \ npx expo run:ios --device ``` @@ -124,8 +130,6 @@ npx expo run:ios --device 앱이 한 번 설치된 뒤 JavaScript 코드만 수정할 때는 네이티브 재빌드 없이 Metro를 다시 연결할 수 있습니다. ```bash -EXPO_PUBLIC_SOUNDLOG_API_SOURCE=server \ -EXPO_PUBLIC_SOUNDLOG_API_BASE_URL=http://:4000 \ npx expo start --dev-client --lan ``` @@ -136,16 +140,16 @@ LAN 연결이 불안정하면 Metro에만 `--tunnel`을 사용할 수 있습니 배포 API가 정상일 때는 다음 주소를 사용합니다. ```text -https://api.soundlog.shop +https://api.soundlog.p-e.kr ``` 앱 실행 전에 먼저 상태를 확인합니다. ```bash -curl https://api.soundlog.shop/v1/health +curl https://api.soundlog.p-e.kr/v1/health ``` -DNS 오류나 타임아웃이 발생하면 배포 API 테스트를 중단하고 로컬 LAN API를 사용하거나 도메인과 GCP API 상태를 먼저 복구합니다. 도메인 장애 중에 GCP IP를 앱에 임시 하드코딩하지 않습니다. +DNS 오류나 타임아웃이 발생하면 배포 API 테스트를 중단하고 로컬 LAN API를 사용하거나 운영 API 상태를 먼저 복구합니다. 도메인 장애 중에 서버 IP를 앱에 임시 하드코딩하지 않습니다. ## 7. EAS 내부 배포 @@ -158,7 +162,7 @@ npx eas-cli device:create npm run build:dev:ios ``` -빌드가 완료되면 표시되는 링크나 QR 코드로 iPhone에 설치합니다. 현재 `development` profile은 `https://api.soundlog.shop`을 직접 사용하므로 도메인 상태가 정상이어야 전체 기능을 테스트할 수 있습니다. +빌드가 완료되면 표시되는 링크나 QR 코드로 iPhone에 설치합니다. 현재 `development` profile은 `https://api.soundlog.p-e.kr`을 직접 사용하므로 도메인 상태가 정상이어야 전체 기능을 테스트할 수 있습니다. 설치 후 로컬 코드를 연결하려면 다음을 실행합니다. diff --git a/docs/deployment/SOUNDLOG_SHOP_DOMAIN.md b/docs/deployment/SOUNDLOG_SHOP_DOMAIN.md index 05f94e6..c4fd683 100644 --- a/docs/deployment/SOUNDLOG_SHOP_DOMAIN.md +++ b/docs/deployment/SOUNDLOG_SHOP_DOMAIN.md @@ -1,43 +1,29 @@ -# Soundlog app deployment domain +# Soundlog 앱 배포 도메인 -Soundlog는 웹 서비스를 운영하거나 배포하지 않습니다. 출시 대상은 Expo/EAS로 빌드한 iOS·Android 네이티브 앱이며, 앱은 GCP API를 직접 호출합니다. Vercel은 운영 배포 경로에 포함하지 않습니다. +Soundlog는 웹 서비스를 운영하지 않습니다. 출시 대상은 Expo와 EAS로 빌드한 iOS 및 Android 네이티브 앱입니다. Vercel은 운영 앱의 네트워크 경로에 포함하지 않습니다. -## Production architecture +## 운영 주소 -| Host | Target | Purpose | +| 주소 | 용도 | 현재 상태 | | --- | --- | --- | -| `api.soundlog.shop` | GCP Compute Engine | 앱 API, 업로드, 개인정보 처리방침, 이용약관, 고객지원 | +| `https://api.soundlog.p-e.kr` | 앱 API, 파일 업로드, ML 음악 추천 | 운영 앱이 직접 호출 | +| `https://api.soundlog.shop/legal/privacy` | 개인정보 처리방침 | EAS 설정에 남아 있으나 현재 DNS 복구 필요 | +| `https://api.soundlog.shop/legal/terms` | 서비스 이용약관 | EAS 설정에 남아 있으나 현재 DNS 복구 필요 | +| `support@soundlog.shop` | 고객지원 | 앱 설정에 표시 | -앱의 모든 네트워크 요청은 `https://api.soundlog.shop`으로 전송됩니다. GCP VM의 Caddy가 HTTPS를 종료하고 Express API로 reverse proxy합니다. +새 API 도메인의 `/v1/health`와 `/openapi.yaml`은 정상 응답합니다. `/v1/recommendations/playlists`는 인증된 요청에 ML 추천을 반환합니다. 새 API 도메인의 `/legal/privacy`, `/legal/terms`, `/support`는 2026년 8월 18일 기준 404입니다. 기존 `api.soundlog.shop`도 같은 날짜 기준 DNS가 해석되지 않습니다. 앱의 API 전환과 별개로 심사 전에 새 서버에 공개 문서 경로를 배포하거나 기존 도메인의 DNS를 복구해야 합니다. -## GCP Cloud DNS +## EAS 앱 환경변수 -GCP project `nomi-app-deploy-2026`의 `soundlog-shop` public zone을 사용합니다. 도메인 등록기관에서 `soundlog.shop`의 네임서버를 아래 값으로 위임합니다. - -- `ns-cloud-d1.googledomains.com` -- `ns-cloud-d2.googledomains.com` -- `ns-cloud-d3.googledomains.com` -- `ns-cloud-d4.googledomains.com` - -Cloud DNS에는 아래 운영 레코드가 등록되어 있습니다. - -| Type | Host | TTL | Value | -| --- | --- | --- | --- | -| `A` | `api` | `300` | `34.64.116.40` | - -기존 AWS Route 53 또는 Vercel 네임서버는 사용하지 않습니다. 등록기관의 네임서버 위임이 Cloud DNS로 바뀐 뒤 Caddy가 `api.soundlog.shop`의 인증서를 자동 발급합니다. - -## EAS app env - -`development`, `preview`, `production` profile은 모두 GCP API를 직접 호출합니다. +`development`, `preview`, `production` profile은 모두 새 API와 ML 서버를 직접 호출합니다. ```dotenv EXPO_PUBLIC_SOUNDLOG_API_SOURCE=server -EXPO_PUBLIC_SOUNDLOG_API_BASE_URL=https://api.soundlog.shop -EXPO_PUBLIC_SOUNDLOG_UPLOAD_ORIGIN=https://api.soundlog.shop +EXPO_PUBLIC_SOUNDLOG_API_BASE_URL=https://api.soundlog.p-e.kr +EXPO_PUBLIC_SOUNDLOG_UPLOAD_ORIGIN=https://api.soundlog.p-e.kr ``` -Production profile은 App Store와 앱 설정에 사용할 공개 법적 문서 URL을 같은 GCP 서버로 지정합니다. +Production profile은 공개 법적 문서와 고객지원 정보를 기존 주소로 유지합니다. ```dotenv EXPO_PUBLIC_SOUNDLOG_PRIVACY_URL=https://api.soundlog.shop/legal/privacy @@ -45,22 +31,14 @@ EXPO_PUBLIC_SOUNDLOG_TERMS_URL=https://api.soundlog.shop/legal/terms EXPO_PUBLIC_SOUNDLOG_SUPPORT_EMAIL=support@soundlog.shop ``` -## Public pages - -- 개인정보 처리방침: `https://api.soundlog.shop/legal/privacy` -- 서비스 이용약관: `https://api.soundlog.shop/legal/terms` -- 고객지원: `https://api.soundlog.shop/support` - -세 페이지는 로그인 없이 접근할 수 있어야 하며 SoundLogServer가 직접 HTML로 제공합니다. - -## Verification +## 검증 ```bash -dig +short soundlog.shop NS -dig +short api.soundlog.shop A -curl https://api.soundlog.shop/v1/health +curl https://api.soundlog.p-e.kr/v1/health +curl -I https://api.soundlog.p-e.kr/openapi.yaml +SOUNDLOG_API_ORIGIN=https://api.soundlog.p-e.kr npm run check:api-origin curl -I https://api.soundlog.shop/legal/privacy curl -I https://api.soundlog.shop/legal/terms -curl -I https://api.soundlog.shop/support -SOUNDLOG_API_ORIGIN=https://api.soundlog.shop npm run check:api-origin ``` + +`check:api-origin`은 임시 계정을 생성해 인증 API와 ML 음악 추천을 확인한 뒤 계정을 즉시 삭제합니다. diff --git a/eas.json b/eas.json index 5adb898..207ab8c 100644 --- a/eas.json +++ b/eas.json @@ -8,28 +8,28 @@ "developmentClient": true, "distribution": "internal", "env": { - "EXPO_PUBLIC_SOUNDLOG_API_BASE_URL": "https://api.soundlog.shop", + "EXPO_PUBLIC_SOUNDLOG_API_BASE_URL": "https://api.soundlog.p-e.kr", "EXPO_PUBLIC_SOUNDLOG_API_SOURCE": "server", - "EXPO_PUBLIC_SOUNDLOG_UPLOAD_ORIGIN": "https://api.soundlog.shop" + "EXPO_PUBLIC_SOUNDLOG_UPLOAD_ORIGIN": "https://api.soundlog.p-e.kr" } }, "preview": { "distribution": "internal", "env": { - "EXPO_PUBLIC_SOUNDLOG_API_BASE_URL": "https://api.soundlog.shop", + "EXPO_PUBLIC_SOUNDLOG_API_BASE_URL": "https://api.soundlog.p-e.kr", "EXPO_PUBLIC_SOUNDLOG_API_SOURCE": "server", - "EXPO_PUBLIC_SOUNDLOG_UPLOAD_ORIGIN": "https://api.soundlog.shop" + "EXPO_PUBLIC_SOUNDLOG_UPLOAD_ORIGIN": "https://api.soundlog.p-e.kr" } }, "production": { "autoIncrement": true, "env": { - "EXPO_PUBLIC_SOUNDLOG_API_BASE_URL": "https://api.soundlog.shop", + "EXPO_PUBLIC_SOUNDLOG_API_BASE_URL": "https://api.soundlog.p-e.kr", "EXPO_PUBLIC_SOUNDLOG_API_SOURCE": "server", "EXPO_PUBLIC_SOUNDLOG_PRIVACY_URL": "https://api.soundlog.shop/legal/privacy", "EXPO_PUBLIC_SOUNDLOG_SUPPORT_EMAIL": "support@soundlog.shop", "EXPO_PUBLIC_SOUNDLOG_TERMS_URL": "https://api.soundlog.shop/legal/terms", - "EXPO_PUBLIC_SOUNDLOG_UPLOAD_ORIGIN": "https://api.soundlog.shop" + "EXPO_PUBLIC_SOUNDLOG_UPLOAD_ORIGIN": "https://api.soundlog.p-e.kr" } } }, diff --git a/scripts/check-api-origin.js b/scripts/check-api-origin.js index 445a1fa..e55f5b5 100644 --- a/scripts/check-api-origin.js +++ b/scripts/check-api-origin.js @@ -3,9 +3,11 @@ const errors = []; const apiOrigin = (process.argv[2] || process.env.SOUNDLOG_API_ORIGIN || '').replace(/\/+$/, ''); let authHeaderPromise; +let disposableAuthHeader; +let shouldDeleteSmokeAccount = false; if (!apiOrigin) { - console.error('Usage: npm run check:api-origin -- https://api.soundlog.shop'); + console.error('Usage: npm run check:api-origin -- https://api.soundlog.p-e.kr'); process.exit(1); } @@ -56,8 +58,11 @@ async function postJson(path, body) { async function createAuthHeader() { const { email, password } = createSmokeCredentials(); + const usesConfiguredCredentials = Boolean( + process.env.SOUNDLOG_CHECK_EMAIL && process.env.SOUNDLOG_CHECK_PASSWORD, + ); - if (process.env.SOUNDLOG_CHECK_EMAIL && process.env.SOUNDLOG_CHECK_PASSWORD) { + if (usesConfiguredCredentials) { try { const login = await postJson('/v1/auth/login', { email, password }); return `Bearer ${login.data.accessToken}`; @@ -70,9 +75,18 @@ async function createAuthHeader() { displayName: 'Soundlog API Check', email, password, + termsAccepted: true, + termsVersion: '2026-08-15', }); - return `Bearer ${register.data.accessToken}`; + const authHeader = `Bearer ${register.data.accessToken}`; + + if (!usesConfiguredCredentials) { + disposableAuthHeader = authHeader; + shouldDeleteSmokeAccount = true; + } + + return authHeader; } async function getAuthHeader() { @@ -162,6 +176,60 @@ async function verifyMusicMetadata() { } } +async function verifyMlRecommendation() { + try { + const payload = await fetchAuthenticatedJson( + '/v1/recommendations/playlists?mood=%EC%9E%94%EC%9E%94%ED%95%9C&state=%EB%B0%94%EB%8B%A4&x=129.1186&y=35.1532', + ); + const recommendation = payload?.data; + + if (recommendation?.context?.source !== 'ml-recommendation') { + addError( + `/v1/recommendations/playlists did not return the ML source: ${JSON.stringify( + recommendation?.context ?? null, + )}`, + ); + } + + if (!Array.isArray(recommendation?.tracks) || recommendation.tracks.length === 0) { + addError('/v1/recommendations/playlists returned no recommended tracks.'); + } + + if (!recommendation?.coverImageUrl?.startsWith('https://')) { + addError('/v1/recommendations/playlists returned no HTTPS playlist cover image.'); + } + } catch (error) { + addError(error instanceof Error ? error.message : String(error)); + } +} + +async function deleteDisposableSmokeAccount() { + if (!shouldDeleteSmokeAccount || !disposableAuthHeader) { + return; + } + + try { + const { response, text, url } = await fetchText('/v1/me', { + headers: { + Authorization: disposableAuthHeader, + }, + method: 'DELETE', + }); + + if (!response.ok) { + addError( + `${url} failed to delete the disposable smoke account: HTTP ${response.status}: ${text.slice(0, 180)}`, + ); + } + } catch (error) { + addError( + `Failed to delete the disposable smoke account: ${ + error instanceof Error ? error.message : String(error) + }`, + ); + } +} + async function verifyLegacyMusicPlatformRouteRemoved() { try { const { response, text, url } = await fetchText('/v1/me/music-platform'); @@ -180,11 +248,16 @@ async function verifyLegacyMusicPlatformRouteRemoved() { } async function main() { - await verifyHealth(); - await verifyOpenApi(); - await verifyNearbyPlaces(); - await verifyMusicMetadata(); - await verifyLegacyMusicPlatformRouteRemoved(); + try { + await verifyHealth(); + await verifyOpenApi(); + await verifyNearbyPlaces(); + await verifyMusicMetadata(); + await verifyMlRecommendation(); + await verifyLegacyMusicPlatformRouteRemoved(); + } finally { + await deleteDisposableSmokeAccount(); + } if (errors.length > 0) { console.error('SoundLog API origin check failed:'); diff --git a/scripts/check-store-release.js b/scripts/check-store-release.js index 374e239..7787e7a 100644 --- a/scripts/check-store-release.js +++ b/scripts/check-store-release.js @@ -116,20 +116,20 @@ function assertProductionEnv(productionEnv) { addError('EAS production env must set EXPO_PUBLIC_SOUNDLOG_SUPPORT_EMAIL to a real mailbox.'); } - if (apiBaseUrl !== 'https://api.soundlog.shop') { - addError('EAS production API must call the GCP api.soundlog.shop origin directly.'); + if (apiBaseUrl !== 'https://api.soundlog.p-e.kr') { + addError('EAS production API must call the current Soundlog API and ML origin directly.'); } - if (uploadOrigin !== 'https://api.soundlog.shop') { - addError('EAS production uploads must use the GCP api.soundlog.shop origin directly.'); + if (uploadOrigin !== 'https://api.soundlog.p-e.kr') { + addError('EAS production uploads must use the current Soundlog API origin directly.'); } if (privacyUrl !== 'https://api.soundlog.shop/legal/privacy') { - addError('EAS production privacy URL must be hosted by the GCP API server.'); + addError('EAS production privacy URL must stay on the existing legal-document origin.'); } if (termsUrl !== 'https://api.soundlog.shop/legal/terms') { - addError('EAS production terms URL must be hosted by the GCP API server.'); + addError('EAS production terms URL must stay on the existing legal-document origin.'); } } diff --git a/scripts/check-vercel-config.js b/scripts/check-vercel-config.js index 8535398..2c008d6 100644 --- a/scripts/check-vercel-config.js +++ b/scripts/check-vercel-config.js @@ -29,9 +29,9 @@ async function main() { addError('Missing vercel.mjs.'); } - ['52.79.185.121', '54.226.62.131', 'api.soundlog.shop'].forEach((marker) => { + ['52.79.185.121', '54.226.62.131', 'api.soundlog.shop', 'api.soundlog.p-e.kr'].forEach((marker) => { if (configText.includes(marker)) { - addError(`vercel.mjs must not hard-code stale API origin ${marker}.`); + addError(`vercel.mjs must not hard-code API origin ${marker}.`); } }); diff --git a/scripts/require-vercel-api-origin.js b/scripts/require-vercel-api-origin.js index 11c219e..00633ef 100644 --- a/scripts/require-vercel-api-origin.js +++ b/scripts/require-vercel-api-origin.js @@ -3,11 +3,12 @@ const apiOrigin = process.env.SOUNDLOG_API_ORIGIN?.replace(/\/+$/, ''); const staleOrigins = new Set([ 'http://52.79.185.121:4000', + 'https://api.soundlog.shop', ]); if (!apiOrigin) { console.error( - 'SOUNDLOG_API_ORIGIN is required for Vercel builds. Set it to the current SoundLogServer origin, for example https://api.soundlog.shop.', + 'SOUNDLOG_API_ORIGIN is required for Vercel builds. Set it to the current SoundLogServer origin, for example https://api.soundlog.p-e.kr.', ); process.exit(1); } From 23dc26762bf3802133f4b2b78c8d7d629ba878cb Mon Sep 17 00:00:00 2001 From: manNomi Date: Thu, 20 Aug 2026 23:08:58 +0900 Subject: [PATCH 2/5] =?UTF-8?q?fix:=20=EC=8B=AC=EC=82=AC=EC=9A=A9=20?= =?UTF-8?q?=EB=B2=95=EC=A0=81=20=EC=A3=BC=EC=86=8C=EC=99=80=20=ED=85=8D?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EB=8C=80=EB=B9=84=20=EB=B3=B4=EA=B0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AGENTS.md | 11 + README.md | 8 +- app.config.js | 8 +- app/auth/callback.tsx | 2 +- app/auth/login.tsx | 2 +- docs/deployment/SOUNDLOG_SHOP_DOMAIN.md | 18 +- .../SOUNDLOG_RN_DESIGN_SYSTEM.md | 8 +- docs/frontend/MAIN_PAGE_RN_BUILD_DOC.md | 2 +- ...08-15-app-store-review-remediation-plan.md | 428 +++++++++--------- eas.json | 4 +- scripts/check-store-release.js | 14 +- src/components/EmptyState.tsx | 2 +- src/components/MiniPlayer.tsx | 4 +- src/components/dev/DevTestManager.tsx | 6 +- src/components/home/CurrentSoundtrackCard.tsx | 4 +- .../home/HomeSoundtrackBottomSheet.tsx | 2 +- src/components/home/MusicLogCard.tsx | 6 +- src/components/home/TravelModeHomeView.tsx | 14 +- .../home/TravelModeSuggestionSheet.tsx | 4 +- src/components/library/LibraryScreen.tsx | 4 +- .../moderation/ReportContentSheet.tsx | 2 +- .../moment-capture/CameraPermissionState.tsx | 2 +- .../moment-capture/MomentCaptureScreen.tsx | 2 +- .../moment-capture/MomentPhotoCanvas.tsx | 34 +- .../moment-capture/MomentReviewPanel.tsx | 10 +- src/components/my/RecapLogGuide.tsx | 2 +- .../onboarding/OnboardingScreen.tsx | 8 +- .../playlist/PlaylistMusicFilter.tsx | 2 +- src/components/playlist/PlaylistState.tsx | 2 +- .../recap-share/RecapPreviewCard.tsx | 30 +- src/components/recap-share/RecapRouteMap.tsx | 2 +- .../recap-share/RecapShareScreen.tsx | 2 +- .../recap-share/RecapShareState.tsx | 2 +- .../recap-share/RecapTemplateSelector.tsx | 2 +- .../recap-share/RecapTravelSummaryCard.tsx | 2 +- .../recap-share/ShareActionList.tsx | 6 +- src/components/recap/RecapListScreen.tsx | 2 +- src/components/travel/CommunityRecapCard.tsx | 10 +- .../travel/EndTravelConfirmModal.tsx | 2 +- .../travel/TravelModeBottomSheet.tsx | 4 +- .../travel/TravelRoomDetailScreen.tsx | 6 +- src/components/travel/TravelStatusCard.tsx | 6 +- .../live-sound-map/LiveSoundMapSection.tsx | 14 +- .../travel/live-sound-map/SoundMapView.tsx | 2 +- .../travel/recap-map/RecapMapSection.tsx | 2 +- src/constants/colors.ts | 8 +- src/constants/legal.ts | 7 +- src/design-system/primitives.tsx | 10 +- src/design-system/recipes.ts | 6 +- tailwind.config.js | 9 +- 50 files changed, 380 insertions(+), 369 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 447bee9..003fbf5 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -19,6 +19,17 @@ - API health checks and command-line diagnostics may support investigation, but they do not replace simulator verification and must not be reported as completed app testing. - Use a web target only for explicit web build or export compatibility work. Even then, do not use it for product acceptance testing unless the user's latest message explicitly overrides this rule. +## Text Color + +- 기본 사용자 노출 텍스트와 버튼, 탭, 칩의 인터랙션 라벨은 흰색 또는 흰색 투명도 계열을 사용한다. 상태를 구분하는 의미 색상과 브랜드 강조 색상은 예외로 둔다. +- `text-black`, 검정색, 진한 보라색 텍스트 값을 새로 추가하지 않는다. +- 기본, 보조, 비활성 텍스트의 명도 차이는 검정색이나 회색 고정값이 아니라 `#FFFFFF` 또는 `rgba(255,255,255,alpha)`로 표현한다. +- 밝은 강조 배경에서 검정색 텍스트로 대비를 해결하지 않는다. 흰색 텍스트가 읽히도록 배경과 테두리 대비를 조정한다. +- `colors.text.inverse`와 `text-soundlog-inverse`는 흰색 의미를 유지한다. +- 사용자에게 보이는 문자열은 React Native의 `Text`를 직접 사용하지 않고 기본 글자색이 흰색인 `AppText`를 사용한다. +- `text-white/52`처럼 기본 Tailwind에 없는 투명도를 사용하면 `tailwind.config.js`의 opacity에도 같은 값을 선언해 검정색 기본값으로 떨어지지 않게 한다. +- UI 변경 후 `text-black`, `color: 'black'`, `#000000`, `#4A1D96`가 텍스트 색상으로 추가되지 않았는지 검색한다. 그림자, 배경, 아이콘, 지도 스타일은 이 검사 대상이 아니다. + ## Recap And Log Domain - Before changing Recap, Log, camera capture, travel mode, route tracking, map pins, visibility, or related API behavior, read `docs/product/RECAP_LOG_DOMAIN_MODEL.md`. diff --git a/README.md b/README.md index 4488132..ecc6c1c 100644 --- a/README.md +++ b/README.md @@ -55,9 +55,9 @@ Soundlog는 웹 서비스를 배포하지 않습니다. 운영 배포 대상은 본인 iPhone을 USB로 연결해 최신 코드를 직접 테스트할 때는 [iPhone 실기기 테스트 가이드](docs/deployment/IOS_PHYSICAL_DEVICE_TESTING.md)를 먼저 확인합니다. 실기기에서 `127.0.0.1`은 Mac이 아니라 iPhone 자신이므로, 로컬 서버를 사용할 때는 Mac의 LAN IP를 앱 빌드 환경변수로 지정해야 합니다. - API 및 ML: `https://api.soundlog.p-e.kr` -- 개인정보 처리방침: `https://api.soundlog.shop/legal/privacy` -- 서비스 이용약관: `https://api.soundlog.shop/legal/terms` -- 고객지원: `support@soundlog.shop` +- 개인정보 처리방침: `https://api.soundlog.p-e.kr/legal/privacy` +- 서비스 이용약관: `https://api.soundlog.p-e.kr/legal/terms` +- 고객지원: 실제 수신과 답장이 확인된 메일을 production EAS 환경에 설정 - API source: `server` - auth: Soundlog 자체 이메일/비밀번호 로그인 - iOS/Android: HTTPS API만 사용 @@ -80,7 +80,7 @@ Android 지인 테스트용 내부 배포 빌드는 아래 명령으로 생성 npx eas build --profile preview --platform android ``` -iOS는 TestFlight 또는 ad hoc 기기 등록이 필요합니다. App Store/TestFlight에 올릴 production profile도 현재는 `https://api.soundlog.p-e.kr`을 사용합니다. 개인정보 처리방침과 이용약관은 EAS 설정에서 기존 `api.soundlog.shop` 주소를 유지하지만 현재 기존 도메인은 DNS가 해석되지 않고 새 도메인의 공개 문서 경로도 404입니다. 심사 전에는 새 서버에 공개 문서를 배포하거나 기존 도메인의 DNS를 복구해야 합니다. +iOS는 TestFlight 또는 ad hoc 기기 등록이 필요합니다. App Store/TestFlight에 올릴 production profile은 API와 개인정보 처리방침과 이용약관에 `https://api.soundlog.p-e.kr`을 사용합니다. 새 서버에 공개 문서 경로를 배포하고 실제 수신 가능한 고객지원 메일을 설정한 뒤 릴리스 검사를 통과해야 합니다. ## 문서 diff --git a/app.config.js b/app.config.js index 1d46967..bfc65f7 100644 --- a/app.config.js +++ b/app.config.js @@ -182,9 +182,13 @@ const baseConfig = { }, extra: { legal: { - privacyPolicyUrl: process.env.EXPO_PUBLIC_SOUNDLOG_PRIVACY_URL, + privacyPolicyUrl: + process.env.EXPO_PUBLIC_SOUNDLOG_PRIVACY_URL ?? + 'https://api.soundlog.p-e.kr/legal/privacy', supportEmail: process.env.EXPO_PUBLIC_SOUNDLOG_SUPPORT_EMAIL ?? 'support@soundlog.shop', - termsUrl: process.env.EXPO_PUBLIC_SOUNDLOG_TERMS_URL, + termsUrl: + process.env.EXPO_PUBLIC_SOUNDLOG_TERMS_URL ?? + 'https://api.soundlog.p-e.kr/legal/terms', }, eas: { projectId: '4b07627b-36bf-463d-a15e-b4839022ecbb', diff --git a/app/auth/callback.tsx b/app/auth/callback.tsx index 9b401b7..7664377 100644 --- a/app/auth/callback.tsx +++ b/app/auth/callback.tsx @@ -37,7 +37,7 @@ export default function AuthCallbackScreen() { router.replace('/auth/login' as never)} > diff --git a/app/auth/login.tsx b/app/auth/login.tsx index 9c4f987..fb46562 100644 --- a/app/auth/login.tsx +++ b/app/auth/login.tsx @@ -273,7 +273,7 @@ export default function LoginScreen() { { void handleSubmit(); diff --git a/docs/deployment/SOUNDLOG_SHOP_DOMAIN.md b/docs/deployment/SOUNDLOG_SHOP_DOMAIN.md index c4fd683..468418c 100644 --- a/docs/deployment/SOUNDLOG_SHOP_DOMAIN.md +++ b/docs/deployment/SOUNDLOG_SHOP_DOMAIN.md @@ -7,11 +7,11 @@ Soundlog는 웹 서비스를 운영하지 않습니다. 출시 대상은 Expo와 | 주소 | 용도 | 현재 상태 | | --- | --- | --- | | `https://api.soundlog.p-e.kr` | 앱 API, 파일 업로드, ML 음악 추천 | 운영 앱이 직접 호출 | -| `https://api.soundlog.shop/legal/privacy` | 개인정보 처리방침 | EAS 설정에 남아 있으나 현재 DNS 복구 필요 | -| `https://api.soundlog.shop/legal/terms` | 서비스 이용약관 | EAS 설정에 남아 있으나 현재 DNS 복구 필요 | -| `support@soundlog.shop` | 고객지원 | 앱 설정에 표시 | +| `https://api.soundlog.p-e.kr/legal/privacy` | 개인정보 처리방침 | 최신 서버 배포 후 사용하는 공개 문서 | +| `https://api.soundlog.p-e.kr/legal/terms` | 서비스 이용약관 | 최신 서버 배포 후 사용하는 공개 문서 | +| production EAS 지원 메일 | 고객지원 | 실제 수신과 답장이 확인된 주소를 설정 | -새 API 도메인의 `/v1/health`와 `/openapi.yaml`은 정상 응답합니다. `/v1/recommendations/playlists`는 인증된 요청에 ML 추천을 반환합니다. 새 API 도메인의 `/legal/privacy`, `/legal/terms`, `/support`는 2026년 8월 18일 기준 404입니다. 기존 `api.soundlog.shop`도 같은 날짜 기준 DNS가 해석되지 않습니다. 앱의 API 전환과 별개로 심사 전에 새 서버에 공개 문서 경로를 배포하거나 기존 도메인의 DNS를 복구해야 합니다. +새 API 도메인의 `/v1/health`와 `/openapi.yaml`은 정상 응답합니다. `/v1/recommendations/playlists`는 인증된 요청에 ML 추천을 반환합니다. 새 API 도메인의 `/legal/privacy`, `/legal/terms`, `/support`는 최신 서버 배포 후 사용합니다. 심사 전에는 세 경로의 200 응답과 고객지원 메일 수신을 확인해야 합니다. ## EAS 앱 환경변수 @@ -26,9 +26,9 @@ EXPO_PUBLIC_SOUNDLOG_UPLOAD_ORIGIN=https://api.soundlog.p-e.kr Production profile은 공개 법적 문서와 고객지원 정보를 기존 주소로 유지합니다. ```dotenv -EXPO_PUBLIC_SOUNDLOG_PRIVACY_URL=https://api.soundlog.shop/legal/privacy -EXPO_PUBLIC_SOUNDLOG_TERMS_URL=https://api.soundlog.shop/legal/terms -EXPO_PUBLIC_SOUNDLOG_SUPPORT_EMAIL=support@soundlog.shop +EXPO_PUBLIC_SOUNDLOG_PRIVACY_URL=https://api.soundlog.p-e.kr/legal/privacy +EXPO_PUBLIC_SOUNDLOG_TERMS_URL=https://api.soundlog.p-e.kr/legal/terms +EXPO_PUBLIC_SOUNDLOG_SUPPORT_EMAIL=실제_수신_가능한_메일 ``` ## 검증 @@ -37,8 +37,8 @@ EXPO_PUBLIC_SOUNDLOG_SUPPORT_EMAIL=support@soundlog.shop curl https://api.soundlog.p-e.kr/v1/health curl -I https://api.soundlog.p-e.kr/openapi.yaml SOUNDLOG_API_ORIGIN=https://api.soundlog.p-e.kr npm run check:api-origin -curl -I https://api.soundlog.shop/legal/privacy -curl -I https://api.soundlog.shop/legal/terms +curl -I https://api.soundlog.p-e.kr/legal/privacy +curl -I https://api.soundlog.p-e.kr/legal/terms ``` `check:api-origin`은 임시 계정을 생성해 인증 API와 ML 음악 추천을 확인한 뒤 계정을 즉시 삭제합니다. diff --git a/docs/design-system/SOUNDLOG_RN_DESIGN_SYSTEM.md b/docs/design-system/SOUNDLOG_RN_DESIGN_SYSTEM.md index 425cc08..809b1d2 100644 --- a/docs/design-system/SOUNDLOG_RN_DESIGN_SYSTEM.md +++ b/docs/design-system/SOUNDLOG_RN_DESIGN_SYSTEM.md @@ -72,7 +72,9 @@ import { | Accent purple | `#7A2CFF` | `text-soundlog-purple` | | Accent gold | `#B1913A` | `text-soundlog-gold` | | Warning action | `#FF8A3D` | `bg-soundlog-warning` | -| Inverse text | `#090515` | `text-soundlog-inverse` | +| Primary action | `#4A1D96` | `bg-soundlog-action` | +| Danger action | `#8E310D` | `bg-soundlog-danger` | +| Inverse text | `#FFFFFF` | `text-soundlog-inverse` | ## Typography @@ -300,9 +302,11 @@ import { AppText, soundlogRecipes } from '@/design-system'; 홈의 `HomeHeader` 패턴을 재사용합니다. - 바깥 트랙: `rounded-full border border-white/10 bg-white/[0.06] p-1` -- 선택 탭: 라임 또는 흰색 배경 +- 선택 탭: 흰색 글자가 읽히는 어두운 강조 배경 - 선택 탭 text: `text-soundlog-inverse` +기본 사용자 노출 텍스트와 인터랙션 라벨은 `AppText`를 사용하고 흰색 또는 흰색 투명도 계열을 적용합니다. 상태를 구분하는 의미 색상과 브랜드 강조 색상은 유지할 수 있습니다. 선택 상태와 CTA는 검정색이나 진한 보라색 글자를 사용하지 않고 `bg-soundlog-selected`, `bg-soundlog-action`, `bg-soundlog-danger`처럼 흰색이 읽히는 배경과 테두리로 대비를 조정합니다. + ### Filter Bar `HomeTopFilterBar`처럼 pill 컨테이너 안에 `Chip`을 가로 스크롤로 배치합니다. diff --git a/docs/frontend/MAIN_PAGE_RN_BUILD_DOC.md b/docs/frontend/MAIN_PAGE_RN_BUILD_DOC.md index 0b67022..33e0cbf 100644 --- a/docs/frontend/MAIN_PAGE_RN_BUILD_DOC.md +++ b/docs/frontend/MAIN_PAGE_RN_BUILD_DOC.md @@ -91,7 +91,7 @@ RN에서는 다음 방식 중 하나를 사용한다. | `background.chip` | `#0E1E3A` | 필터 칩 | | `border.chip` | `#364283` | 칩 테두리 | | `text.primary` | `#FFFFFF` | 주요 텍스트 | -| `text.secondary` | `#ACACAC` | 보조 텍스트 | +| `text.secondary` | `rgba(255,255,255,0.68)` | 보조 텍스트 | | `accent.purple` | `#7A2CFF` | 진행바/선택 포인트 | | `accent.gold` | `#B1913A` | 추천 카드 | | `player.background` | `#45343D` | 미니 플레이어 | diff --git a/docs/implementation/2026-08-15-app-store-review-remediation-plan.md b/docs/implementation/2026-08-15-app-store-review-remediation-plan.md index 4dbe70f..c734b6b 100644 --- a/docs/implementation/2026-08-15-app-store-review-remediation-plan.md +++ b/docs/implementation/2026-08-15-app-store-review-remediation-plan.md @@ -1,306 +1,284 @@ # App Store 심사 거절 대응 계획 -## 1. 문서 목적 +## 1. 목적과 완료 기준 -이 문서는 2026년 8월 14일 App Store 심사에서 지적된 세 가지 문제를 해결하고 새 빌드를 재제출하기 위한 실행 계획이다. +이 문서는 2026년 8월 14일에 반려된 Soundlog `1.0.0 (10)`을 다시 제출하기 위한 실행 계획이다. -대상 제출은 `1.0.0 (10)`이며 제출 ID는 `334b774a-ec5f-454f-9fc8-a629b8b61c56`이다. +대상 제출 ID는 `334b774a-ec5f-454f-9fc8-a629b8b61c56`이며 반려 사유는 다음 세 가지다. -이번 대응의 완료 기준은 코드를 수정하는 데서 끝나지 않는다. 새 빌드에서 기능을 검증하고 App Store Connect의 스크린샷과 심사 메모와 실제 기기 녹화까지 교체한 뒤 심사자가 같은 흐름을 재현할 수 있어야 한다. +- 지침 1.2 사용자 생성 콘텐츠 안전 조치 +- 지침 2.3.3 실제 앱 사용을 보여주지 않는 13인치 iPad 스크린샷 +- 지침 2.3.8 임시 앱 아이콘 -## 2. 심사 지적과 현재 상태 +완료는 코드가 존재하는 상태를 뜻하지 않는다. 심사자가 최종 TestFlight 빌드에서 약관과 필터와 신고와 차단을 재현할 수 있어야 한다. 최종 아이콘과 실제 핵심 기능 스크린샷이 App Store Connect에 반영되어야 한다. 공개 약관과 고객지원 주소도 실제로 열리고 연락을 받을 수 있어야 한다. -| 지침 | Apple 지적 | 현재 코드에서 확인한 상태 | 판단 | -| --- | --- | --- | --- | -| 1.2 사용자 생성 콘텐츠 | 약관과 필터와 신고와 차단과 24시간 대응 체계가 필요함 | 회원가입 약관 동의가 있고 음악 매칭 화면에 신고와 차단 API가 있다. 그러나 약관에 불쾌한 콘텐츠와 악용 사용자를 용납하지 않는다는 문구가 명확하지 않다. 게시 전 콘텐츠 필터가 없다. 신고와 차단은 하나의 버튼으로 묶여 있다. 공개 리캡과 댓글에는 신고와 차단 진입점이 없다. 신고 운영 상태와 24시간 처리 수단도 없다. | 새 빌드 필요 | -| 2.3.3 정확한 메타데이터 | 13인치 iPad 스크린샷이 실제 핵심 기능을 보여주지 않음 | App Store Connect에는 iPhone 이미지 다섯 장과 iPad 온보딩 이미지 한 장이 등록되어 있다. 제출 빌드 `1.0.0 (10)`의 기기 제품군은 iPhone과 iPad다. 현재 소스 설정과 iOS 프로젝트는 이후 iPhone 전용으로 변경되어 있으므로 새 빌드에서 이 설정이 반영되는지 확인해야 한다. | 새 빌드와 App Store Connect 수정 필요 | -| 2.3.8 정확한 메타데이터 | 앱 아이콘이 임시 아이콘으로 보임 | `assets/icon.png`와 네이티브 AppIcon이 Expo 기본 템플릿 아이콘이다. 크기와 투명도 검사만 통과할 뿐 Soundlog 브랜드 아이콘이 아니다. | 새 아이콘과 새 빌드 필요 | +## 2. 2026년 8월 20일 현재 판정 -### 2.1 App Store Connect 실사 결과 - -로그인된 App Store Connect에서 다음 상태를 직접 확인했다. - -- 재제출 버튼은 비활성화되어 있고 앱 버전 수정 링크만 사용할 수 있다. -- iPhone 6.5 디스플레이에는 `01-onboarding`, `02-music-recommendation`, `03-playlist-detail`, `04-library-playlists`, `05-map` 다섯 장이 등록되어 있다. -- 13인치 iPad에는 `01-onboarding` 한 장만 등록되어 있다. -- Apple이 첨부한 iPad 지적 화면도 넓은 빈 배경이 대부분인 온보딩 화면이다. -- Apple이 첨부한 아이콘 지적 화면은 Expo 기본 템플릿 아이콘이다. -- Apple 첨부 세 개 중 iPad 온보딩 화면과 Expo 기본 아이콘은 확인했다. `스크린샷-0814-125651.png`는 App Store Connect에서 다운로드를 반복해도 로컬 파일이 생성되지 않아 원본 확인이 남아 있다. 구현 전에 사용자가 직접 내려받아 제공하면 이 계획과 최종 대조한다. -- 빌드 10의 기기 제품군은 App Store Connect에서 `iPhone, iPad`로 표시된다. -- 심사용 로그인 계정은 `local-demo@soundlog.test`로 등록되어 있다. -- 현재 심사 메모는 위치와 카메라와 공개 리캡만 설명한다. 약관과 필터와 신고와 차단의 진입 경로와 24시간 운영 정책과 실제 기기 영상은 없다. -- 출시 방식은 심사 승인 후 수동 출시로 설정되어 있다. - -### 2.2 공개 지원 채널 실사 결과 - -`api.soundlog.shop`은 현재 `34.64.116.40`으로 해석되며 `/v1/health`, `/legal/terms`, `/support`가 HTTPS 200 응답을 반환한다. 고객지원 페이지에는 `support@soundlog.shop`이 공개되어 있다. - -다만 `soundlog.shop` 최상위 도메인에는 현재 A 레코드와 MX 레코드가 없다. 따라서 페이지에 이메일 주소가 보이는 것과 실제로 신고 메일을 수신할 수 있다는 것은 별개다. 재제출 전에 메일 수신용 MX를 설정하고 외부 주소에서 보낸 시험 메일이 운영 받은편지함에 도착하며 답장까지 가능한지 확인해야 한다. 이 검증 전에는 24시간 대응 채널이 준비되었다고 판단하지 않는다. - -## 3. 해결 원칙 - -사용자 생성 콘텐츠는 텍스트만 뜻하지 않는다. 공개되는 닉네임과 댓글과 리캡 제목과 사진과 현재 음악 정보가 모두 심사 대상이 될 수 있다. - -신고 버튼만 추가해서는 통과 조건을 충족하지 못한다. 게시 전에 차단할 수 있어야 하고 신고가 서버에 저장되어야 하며 운영자가 24시간 안에 처리할 수 있어야 한다. 사용자를 차단하면 해당 사용자의 콘텐츠가 즉시 현재 화면에서 사라지고 이후 서버 응답에서도 제외되어야 한다. - -iPad를 지원하지 않는 현재 제품 방향은 유지한다. 심사에 제출된 빌드 10은 App Store Connect에서 iPhone과 iPad 지원으로 표시되므로 현재 소스의 iPhone 전용 설정을 반영한 새 빌드가 필요하다. 새 빌드가 실제로 iPhone 전용으로 처리되는지 확인한 뒤 잘못 업로드된 iPad 스크린샷을 제거한다. App Store Connect가 새 빌드도 iPad 지원 앱으로 판단한다면 스크린샷만 우회해서 처리하지 않고 빌드 설정부터 바로잡는다. - -### 3.1 Apple 요구사항 추적표 - -| Apple 요구사항 | 구현 범위 | 심사자가 확인할 증거 | +| 영역 | 현재 확인된 사실 | 판정 | | --- | --- | --- | -| 로그인 전 EULA 또는 이용약관 | 로그인 화면에서 약관 열람, 필수 동의, 약관 버전과 동의 시각 서버 저장 | 로그아웃 상태 실제 기기 영상과 사용자 동의 레코드 | -| 불쾌한 콘텐츠와 악용 사용자 무관용 문구 | 앱 약관과 공개 약관 페이지를 같은 버전으로 수정 | 앱 화면과 `api.soundlog.shop/legal/terms` | -| 불쾌한 콘텐츠 필터 | 모든 사용자 작성 텍스트의 서버 필터와 공개 사진 검토 상태 | 거절 API 테스트와 실제 기기 게시 거절 화면 | -| 콘텐츠 신고 | 사용자 생성 콘텐츠별 별도 신고 동작과 사유 입력 | 미리 작성된 공개 콘텐츠의 신고 영상과 신고 레코드 | -| 사용자 차단 | 사용자별 별도 차단 동작과 모든 조회 경로의 서버 필터 | 차단 직후 콘텐츠가 사라지는 영상과 조회 API 테스트 | -| 차단 시 개발자 통지 | 차단과 관련 콘텐츠 사본을 운영 신고 큐에 생성하고 알림 발송 | 운영 알림과 관리자 신고 큐 | -| 24시간 내 삭제와 사용자 제재 | 처리 기한, 콘텐츠 삭제, 사용자 정지, 미처리 경고 | 관리자 처리 기록과 기한 점검 결과 | -| 미리 작성된 콘텐츠 | 전용 심사 계정에 신고와 차단 가능한 타 사용자 콘텐츠 준비 | TestFlight 심사 계정의 실제 화면 | -| 실제 기기 녹화 | TestFlight 설치 iPhone에서 약관, 신고, 차단을 연속 촬영 | 심사 메모에 첨부된 원본 영상 | -| 실제 핵심 기능 스크린샷 | 최종 서버 데이터가 보이는 iPhone 핵심 화면으로 전체 교체 | App Store Connect 미디어 관리자 | -| 최종 앱 아이콘 | Expo 기본 아이콘을 브랜드 아이콘으로 교체하고 새 빌드 생성 | TestFlight, 홈 화면, 새 빌드 아이콘 | -| 지원 연락처 | 공개 지원 페이지와 실제 수신 가능한 운영 메일 | HTTPS 200 응답과 외부 메일 송수신 기록 | - -## 4. 단계별 실행 계획 +| App Store Connect | Chrome에서 제출 상세와 버전 편집 화면을 직접 확인했다. 앱 버전은 `1.0.0 제출 준비 중`이며 `심사 업데이트`는 가능하지만 `앱 심사에 다시 제출`은 아직 비활성화되어 있다. 출시 방식은 수동 출시다. | 읽기 검수 완료 | +| Apple 첨부 자료 | 첫 이미지는 큰 여백의 온보딩 화면이고 두 번째 이미지는 Expo 기본 자리표시자 아이콘이며 세 번째 이미지는 다른 사람과 내 로그가 모두 0개인 빈 로그 화면이다. | 반려 원인 확인 | +| 현재 선택 빌드 | 버전 `1.0.0`에는 빌드 16이 선택되어 있다. 빌드 16의 기기 제품군은 iPhone이지만 App Store Connect에 표시되는 아이콘 자산은 `Placeholder.mill`이다. | 교체 필요 | +| 사용 가능한 최신 빌드 | 빌드 17은 처리 완료와 제출 준비 완료 상태이며 내부 테스트 그룹 3명이 접근할 수 있다. 다만 죽은 약관 주소를 포함하고 현재 로컬 글자색 변경도 포함하지 않는다. | 최종 제출에 사용하지 않음 | +| UGC 앱 기능 | 로그인 전 약관 열람과 필수 동의, 콘텐츠 신고, 사용자 차단 UI가 현재 앱 코드에 있다. | 구현됨 | +| UGC 서버 기능 | 필터, 신고 저장, 차단 반영, 관리자 신고 처리, 24시간 기한 점검, 심사용 시드가 서버 `main`에 있다. | 구현됨 | +| 운영 서버 배포 | `https://api.soundlog.p-e.kr/v1/health`와 데이터베이스는 정상이다. 그러나 운영 서버의 관리자 신고 처리 경로는 모두 404이며 최신 서버의 공개 약관 경로도 404다. | 최신 코드 미배포 | +| 공개 약관 | 빌드 17과 App Store 개인정보 처리방침 필드는 `https://api.soundlog.shop/legal/privacy`를 사용한다. 앱의 이용약관도 `api.soundlog.shop`을 사용하며 이 호스트는 현재 DNS가 해석되지 않는다. | 심사 차단 요소 | +| 고객지원 | 앱은 `support@soundlog.shop`을 표시하지만 `soundlog.shop`에 MX 레코드가 없다. 메일 수신과 답장 가능 여부도 증명되지 않았다. | 심사 차단 요소 | +| App Store 지원 URL | 버전 편집 화면의 지원 URL은 `https://api.soundlog.shop/support`다. 현재 접속할 수 없는 주소다. | 심사 차단 요소 | +| 앱 아이콘 | 소스와 빌드 17의 IPA에는 LP와 지도 핀을 결합한 최종 아이콘이 있다. 그러나 현재 선택된 빌드 16과 App Store 버전에는 Expo 자리표시자 아이콘이 표시된다. | 최종 빌드 선택 후 재확인 필요 | +| iPad 지원 범위 | 소스는 `supportsTablet: false`이며 빌드 16과 빌드 17의 기기 제품군은 iPhone이다. 미디어 관리의 13, 11, 12.9, 10.5, 9.7인치 iPad 이미지가 모두 0개임을 확인했다. | 현재 메타데이터 해결됨 | +| iPhone 스크린샷 | 6.5인치 이미지 다섯 장만 등록되어 6.3인치 이하에 재사용되고 있다. 6.9인치 전용 이미지는 0개다. 현재 순서는 공개 리캡 상세, 음악 추천, 반복 이미지가 많은 공개 로그, 단색 커버 곡 목록, 신고 시트다. | 재촬영과 재정렬 필요 | +| 앱 심사 정보 | 심사 메모에는 약관과 신고와 차단과 24시간 처리 절차가 적혀 있다. 그러나 실제 기기 녹화본은 첨부되지 않았고 암호와 연락처 전화번호와 이메일 입력칸은 브라우저에서 비어 있는 상태로 확인됐다. | 보완 필요 | -### 4.1 App Store Connect 원본 확인 +## 3. 반려 사유별 결론 -로그인된 App Store Connect에서 다음 항목을 먼저 확인한다. +### 3.1 지침 1.2 사용자 생성 콘텐츠 -- Apple이 첨부한 이미지 세 장을 내려받아 각각 어떤 화면과 아이콘을 지적했는지 확인한다. -- 미디어 관리자에서 한국어 스크린샷의 모든 기기 크기를 연다. -- 13인치 iPad에 실제로 등록된 이미지 개수와 출처를 확인한다. -- 빌드 `1.0.0 (10)`의 기기 제품군이 iPhone과 iPad로 확인되었으므로 새 빌드가 iPhone 전용으로 바뀌는지 확인한다. -- 심사 정보에 등록된 데모 계정과 메모와 첨부 파일을 확인한다. -- 앱 정보에 표시되는 아이콘과 빌드 아이콘을 각각 확인한다. +필요한 앱과 서버 기능은 대부분 구현되어 있다. 남은 핵심은 운영 서버 배포와 심사용 데이터와 실제 기기 증거다. -이 단계는 화면을 읽는 작업만 수행한다. 삭제와 업로드와 회신과 재제출은 구현과 검증이 끝난 뒤 별도 확인을 받고 진행한다. +현재 서버 `main`은 다음 항목을 제공한다. -### 4.2 이용약관 동의 증거 강화 +- 로그인과 회원가입 전에 확인할 수 있는 이용약관 +- 약관 버전과 동의 시각 저장 및 미동의 계정 차단 +- 닉네임과 장소명과 곡명과 메모와 댓글과 여행방 제목에 대한 서버 필터 +- 공개 콘텐츠 신고와 사용자 차단 +- 차단 직후 대상 사용자의 콘텐츠를 조회 결과에서 제외하는 처리 +- 신고 접수 시 콘텐츠 사본과 24시간 처리 기한 저장 +- 운영자 신고 목록과 콘텐츠 숨김과 사용자 정지와 해결 처리 +- 20시간 경고와 24시간 초과 점검 +- 심사용 계정과 두 개의 주변 사운드 핀과 공개 리캡과 댓글을 만드는 시드 -수정 대상은 프론트엔드 `app/auth/login.tsx`와 `app/legal/terms.tsx`이며 서버의 회원가입 요청과 사용자 약관 동의 저장 구조도 함께 변경한다. +하지만 운영 서버에는 관리자 신고 처리 API와 공개 약관 경로가 없다. 코드 존재만으로는 Apple 요구사항을 충족했다고 볼 수 없다. 최신 서버 배포와 운영 환경 변수 확인이 첫 번째 실행 작업이다. -이용약관에는 다음 정책을 명확히 쓴다. +### 3.2 지침 2.3.3 정확한 스크린샷 -- 불쾌하거나 폭력적이거나 혐오적이거나 성적인 콘텐츠를 허용하지 않는다. -- 타인을 괴롭히거나 사칭하거나 위협하거나 스팸을 보내는 사용자를 허용하지 않는다. -- 위반 콘텐츠는 삭제될 수 있고 위반 사용자는 서비스 이용이 제한되거나 차단될 수 있다. -- 신고는 `support@soundlog.shop`으로도 접수할 수 있으며 24시간 안에 검토하고 필요한 조치를 한다. +Apple이 첨부한 이미지는 큰 여백의 온보딩 화면과 콘텐츠가 하나도 없는 빈 로그 화면을 보여준다. 두 화면 모두 앱의 핵심 기능과 실제 사용 가치를 설명하지 못한다. -`support@soundlog.shop`은 현재 수신 가능성이 증명되지 않았다. 약관에 24시간 대응을 약속하기 전에 MX 설정과 운영 받은편지함 담당자와 휴일을 포함한 확인 주기를 확정한다. 외부 메일 주소에서 신고 시험 메일을 보내고 수신과 답장을 모두 확인한다. +현재 선택된 빌드 16과 빌드 17은 App Store Connect에서 기기 제품군이 iPhone으로 표시된다. 미디어 관리에서도 모든 iPad 크기의 이미지가 0개다. 이 상태를 유지하고 최종 빌드에서도 iPhone 전용 설정을 다시 확인한다. -회원가입 전에 약관 본문을 열 수 있어야 한다. 필수 동의 체크를 하지 않으면 회원가입이 불가능해야 한다. 서버에는 동의한 약관 버전과 동의 시각을 저장하여 프론트 화면만 조작해서 우회할 수 없도록 한다. +현재 등록된 iPhone 이미지는 6.5인치 다섯 장뿐이며 6.9인치 이미지는 없다. 첫 이미지가 공개 리캡 상세이고 마지막 이미지가 신고 시트다. 공개 로그 화면은 같은 야경 이미지가 반복되고 곡 목록은 앨범 커버 대신 단색 블록을 보여준다. 신고와 차단 화면은 UGC 대응 증거로는 유용하지만 스토어 첫 화면을 차지하면 Soundlog의 핵심 가치가 약해진다. 앱 스크린샷과 심사 증거 영상을 분리한다. -예상 수정 파일은 다음과 같다. +### 3.3 지침 2.3.8 최종 아이콘 -- `app/auth/login.tsx` -- `app/legal/terms.tsx` -- `src/api/authApi.ts` -- `src/types/auth.ts` -- `SoundLogServer/src/validators/api.validators.ts` -- `SoundLogServer/src/services/auth.service.ts` -- `SoundLogServer/prisma/models/auth.prisma` -- 새 Prisma 마이그레이션 +Apple이 첨부한 원본은 파란색 Expo 기본 자리표시자 아이콘이다. 현재 App Store 버전에는 빌드 16이 선택되어 있고 화면에 표시되는 아이콘 URL도 `Placeholder.mill`이다. 따라서 이 항목은 아직 App Store 제출 상태에서는 해결되지 않았다. -### 4.3 게시 전 콘텐츠 필터 구현 +소스와 빌드 17의 IPA에서는 다음 조건을 충족한다. -게시 전 필터는 사용자가 작성한 내용을 저장하기 전에 금지된 내용을 탐지하고 거절하는 기능이다. 프론트엔드의 입력 제한만 믿지 않고 서버에서 최종 판정한다. +- 1024 곱하기 1024 PNG +- 알파 채널 없음 +- Expo 아이콘과 iOS 네이티브 AppIcon 동일 +- LP와 지도 핀을 결합한 Soundlog 브랜드 아이콘 +- Expo 시작 아이콘 해시를 막는 출시 검사 존재 -1차 적용 대상은 닉네임과 여행방 제목과 참여자 표시 이름과 후보 메모와 댓글과 공동 리캡 제목이다. 이 값들이 저장되는 모든 API가 하나의 필터 서비스를 거치도록 한다. +별도의 아이콘 디자인 작업은 필요하지 않다. 빌드 17은 약관 주소와 최신 UI 변경이 빠져 최종 제출에는 사용하지 않는다. 새 최종 빌드를 만들고 그 빌드를 App Store 버전에 선택한 뒤 LP와 지도 핀 아이콘이 빌드 행과 포함된 자산과 앱 메뉴에 모두 표시되는지 확인해야 이 반려 사유가 해결된다. -사진이 공개 리캡으로 노출되는 경로에는 별도의 검토 상태를 둔다. 사진 검토 기능이 준비되기 전까지 공개 전환 요청은 검토 대기 상태로 저장하고 안전 판정이 끝난 콘텐츠만 공개 목록에 포함한다. 비공개 저장은 그대로 허용한다. +## 4. 실행 순서 -예상 서버 변경은 다음과 같다. +### 4.1 App Store Connect 읽기 검수 완료 -- `src/services/content-moderation.service.ts`를 추가한다. -- 금지어와 반복 스팸과 외부 연락처 노출을 검사하는 공통 규칙을 추가한다. -- 이미지 검토 결과를 `pending`, `approved`, `rejected` 상태로 저장한다. -- 필터에 걸린 요청은 사용자가 이해할 수 있는 오류 코드와 문구를 반환한다. -- 우회 입력과 공백 변형과 대소문자 변형을 포함한 테스트를 추가한다. +2026년 8월 20일 Chrome에서 다음 항목을 읽기 전용으로 확인했다. -프론트엔드는 서버 오류를 일반 네트워크 오류로 숨기지 않고 해당 문구를 수정하도록 안내한다. +1. Apple 첨부 이미지 세 장은 온보딩 화면과 Expo 자리표시자 아이콘과 빈 로그 화면이다. +2. 현재 선택 빌드는 16이며 빌드 17은 TestFlight에서 제출 준비 완료 상태다. +3. 모든 iPad 크기의 스크린샷은 0개다. +4. iPhone 6.5인치에는 다섯 장이 있고 6.9인치에는 0개다. +5. 심사 메모는 작성되어 있지만 실제 기기 영상 첨부는 없다. +6. 심사 암호와 연락처 전화번호와 이메일 칸은 브라우저에서 비어 있다. +7. 개인정보 처리방침과 지원 URL은 모두 접속할 수 없는 `api.soundlog.shop`을 사용한다. +8. 현재 App Store 아이콘은 자리표시자이며 수동 출시가 선택되어 있다. +9. `심사 업데이트`는 가능하지만 `앱 심사에 다시 제출` 버튼은 아직 비활성화되어 있다. -### 4.4 신고 기능을 모든 사용자 콘텐츠에 연결 +이 검수에서는 이미지 삭제와 업로드와 빌드 교체와 Apple 회신과 재제출을 수행하지 않았다. -현재 음악 매칭 화면의 `차단/신고` 결합 버튼을 `신고`와 `사용자 차단`으로 분리한다. 신고는 사유 선택과 선택 설명을 받는 바텀 시트를 사용한다. +### 4.2 운영 서버 배포 경로 복구 -신고 진입점은 다음 화면에 제공한다. +현재 `SoundLogServer/main`에는 PR 검사만 있고 GCP 배포 워크플로 파일이 없다. 마지막 GCP 자동 배포는 2026년 8월 1일에 실행되었다. 이후 UGC 안전 기능과 지역 데이터와 이미지 검수 변경은 운영 서버에 올라가지 않았다. -- 주변 사운드 사용자 카드 -- 공개 리캡 목록과 공개 리캡 상세 -- 공동 여행방의 다른 사용자 댓글과 후보 리캡 -- 받은 동행 요청 +다음 작업을 수행한다. -신고 대상에는 대상 사용자와 콘텐츠 종류와 콘텐츠 ID와 당시 콘텐츠 사본을 함께 저장한다. 음악 핀 ID만 전달된 경우에도 서버가 소유 사용자를 찾아 `targetUserId`를 반드시 채운다. +1. 기존 GCP 배포 워크플로를 현재 `main` 기준으로 복구한다. +2. 공개 API 기준 주소를 `https://api.soundlog.p-e.kr`로 변경한다. +3. 배포에 필요한 Caddy 설정과 운영용 Docker Compose 파일을 현재 저장소에 맞춘다. +4. 운영 환경에 `MODERATION_ADMIN_KEY`, 신고 알림 설정, 심사용 계정 정보가 존재하는지 값은 노출하지 않고 확인한다. +5. 데이터베이스 마이그레이션과 심사용 시드 실행 순서를 배포 절차에 포함한다. +6. `main`의 현재 커밋을 운영 서버에 배포한다. -예상 프론트엔드 변경은 다음과 같다. +배포 후 다음 결과가 나와야 한다. -- `src/components/moderation/ReportContentSheet.tsx`를 추가한다. -- `src/components/travel/live-sound-map/LiveSoundMapSection.tsx` -- `src/components/travel/recap-map/SelectedRecapPinPanel.tsx` -- `src/components/travel/CommunityRecapCard.tsx` -- `src/components/travel/TravelRoomDetailScreen.tsx` -- `src/api/communityApi.ts` -- `src/types/domain.ts` +- `/v1/health`는 200과 데이터베이스 정상 상태 +- `/legal/privacy`, `/legal/terms`, `/support`는 200 +- 인증 없는 `/v1/admin/moderation/reports`는 경로 없음 404가 아니라 인증 실패 응답 +- `/openapi.yaml`에는 신고와 차단과 관리자 신고 처리 경로가 모두 포함됨 +- 심사용 계정으로 로그인할 수 있고 두 개 이상의 주변 사운드가 보임 -### 4.5 사용자 차단과 즉시 숨김 보장 +### 4.3 공개 약관과 고객지원 정상화 -차단 버튼에는 확인 화면을 제공한다. 사용자가 차단을 확정하면 다음 결과가 즉시 발생해야 한다. +가장 단순한 방향은 죽은 `api.soundlog.shop`을 계속 유지하지 않고 현재 API 주소에서 공개 문서를 제공하는 것이다. -- 현재 화면에서 대상 사용자의 카드와 핀과 댓글과 공개 리캡이 사라진다. -- 서버의 주변 사운드와 공개 리캡과 여행방 조회에서도 대상 사용자의 콘텐츠가 제외된다. -- 양방향의 대기 중 동행 요청이 취소된다. -- 차단 작업과 관련 신고가 운영자 큐에 기록된다. +권장 주소는 다음과 같다. -현재 서버는 주변 음악 핀 일부에서 차단 사용자를 제외하지만 공개 리캡과 여행방 댓글 전체에 같은 규칙이 적용되었다는 증거가 없다. 조회 서비스마다 흩어진 조건을 공통 차단 범위 함수로 묶고 API 테스트로 증명한다. +- 개인정보 처리방침: `https://api.soundlog.p-e.kr/legal/privacy` +- 서비스 이용약관: `https://api.soundlog.p-e.kr/legal/terms` +- 고객지원 페이지: `https://api.soundlog.p-e.kr/support` -### 4.6 24시간 운영 처리 체계 구축 +서버 배포 후 앱과 출시 검사와 문서와 App Store Connect의 개인정보 처리방침 및 지원 URL을 이 주소로 맞춘다. -신고 레코드에는 처리 상태와 처리 기한과 담당자와 처리 결과를 저장한다. 단순히 신고가 데이터베이스에 쌓이는 상태로는 완료로 보지 않는다. +수정 대상은 다음과 같다. -운영자가 수행할 수 있어야 하는 작업은 다음과 같다. +- `eas.json` +- `app.config.js`의 안전한 기본값 +- `scripts/check-store-release.js` +- `README.md` +- 서버의 공개 문서와 OpenAPI 기준 주소 -- 대기 중 신고를 오래된 순서로 조회한다. -- 신고 당시 콘텐츠를 확인한다. -- 콘텐츠를 숨기거나 삭제한다. -- 위반 사용자를 정지하거나 차단한다. -- 신고를 해결 처리하고 사유를 기록한다. +지원 이메일은 실제 수신 가능한 주소를 사용해야 한다. `support@soundlog.shop`을 계속 사용하려면 먼저 MX와 받은편지함을 설정해야 한다. 그렇지 않으면 사용자가 지정한 실제 운영 메일로 앱과 서버 약관과 지원 페이지를 함께 바꾼다. 외부 메일에서 시험 문의를 보내고 수신과 답장까지 확인한다. -새 신고가 들어오면 운영 메일로 알림을 보낸다. 접수 후 20시간이 지나도록 해결되지 않은 신고는 다시 알림을 보내고 24시간을 넘긴 신고를 운영 상태 점검에서 실패로 표시한다. +### 4.4 운영 환경 UGC 검증 -초기 운영 도구는 외부 공개 관리자 페이지가 아니라 인증된 관리자 API와 실행 문서로 구성해도 된다. 다만 운영자가 실제로 매일 확인할 수 있고 삭제와 사용자 정지까지 실행할 수 있어야 한다. +최신 서버 배포 후 운영 환경을 대상으로 다음 시나리오를 검증한다. -예상 서버 변경은 다음과 같다. +1. 약관에 동의하지 않은 등록과 로그인이 거절된다. +2. 금지 문구와 공백이나 기호로 우회한 문구가 서버에서 거절된다. +3. 정상 콘텐츠는 저장된다. +4. 다른 사용자의 공개 리캡과 댓글과 주변 사운드를 신고할 수 있다. +5. 사용자를 차단하면 해당 콘텐츠가 현재 화면에서 즉시 사라진다. +6. 새로 조회해도 차단 사용자의 콘텐츠가 다시 나타나지 않는다. +7. 차단과 신고가 운영 큐에 기록되고 알림이 발생한다. +8. 운영자가 콘텐츠를 숨기고 사용자를 정지하고 신고를 해결할 수 있다. +9. 20시간 경고와 24시간 초과 점검 작업이 실행된다. +10. 심사용 시드를 다시 실행하면 이전 차단과 신고 상태가 초기화되고 데모 콘텐츠가 복구된다. -- `CommunityReport`에 상태와 기한과 처리 정보와 콘텐츠 사본을 추가한다. -- `User`에 서비스 정지 상태와 정지 사유와 시각을 추가한다. -- 관리자 인증 미들웨어와 신고 목록 및 해결 API를 추가한다. -- 신고 알림 서비스와 만료 점검 작업을 추가한다. -- 신고 접수와 차단과 삭제와 사용자 정지에 대한 API 테스트를 추가한다. +서버 자동 검사는 타입 검사와 빌드와 OpenAPI 동기화와 mock 데이터베이스 API 전체 테스트를 포함한다. 앱 자동 검사는 타입 검사와 전체 테스트와 출시 검사를 포함한다. -### 4.7 심사용 미리 작성된 콘텐츠 준비 +### 4.5 최종 앱 변경 묶음 확정 -Apple은 안전 기능을 확인할 수 있는 미리 작성된 콘텐츠를 요청했다. 심사용 계정에는 다음 상태를 준비한다. +최종 빌드에는 다음 변경이 한 번에 들어가야 한다. -- 다른 데모 사용자가 작성한 공개 사운드 카드 두 개 이상 -- 신고 가능한 공개 리캡 한 개 이상 -- 다른 사용자가 작성한 공동 여행방 댓글 한 개 이상 -- 차단 후 사라지는 대상 사용자 한 명 +- API와 ML 주소 `https://api.soundlog.p-e.kr` +- 정상 동작하는 공개 약관과 개인정보 처리방침 주소 +- 실제 수신 가능한 고객지원 이메일 +- UGC 안전 기능 +- 최종 Soundlog 아이콘 +- 검정 계열 글자를 흰색으로 바꾼 현재 UI 변경 +- iPhone 전용 설정 -심사자가 운영 데이터나 실제 사용자에게 영향을 주지 않도록 전용 데모 계정과 전용 데모 콘텐츠를 사용한다. 데모 콘텐츠는 매 배포 후 초기화할 수 있어야 한다. +현재 변경은 작업 트리에 커밋되지 않은 상태다. 관련 변경을 코드 리뷰한 뒤 하나의 출시 브랜치에 정리한다. 기존 사용자 작업과 공모전 자료는 포함하지 않는다. -현재 등록된 `local-demo@soundlog.test` 계정은 개발 도구가 최초 로그인 때 생성하는 계정과 같다. 심사 직전에 계정이 존재한다는 사실만 확인하지 말고 공개 콘텐츠와 신고 대상 사용자가 실제로 보이는지 TestFlight에서 다시 확인한다. +빌드 17은 최종 제출용으로 사용하지 않는다. 위 변경을 반영한 다음 빌드 번호를 생성한다. -### 4.8 최종 Soundlog 앱 아이콘 교체 +### 4.6 시뮬레이터와 실제 기기 검수 -현재 아이콘은 Expo 기본 템플릿 아이콘이므로 완전히 교체한다. +Soundlog 제품 검수는 웹에서 하지 않는다. -최종 아이콘은 Soundlog의 장소와 음악과 기록이라는 핵심 인상을 하나의 단순한 기호로 표현한다. 작은 홈 화면 크기에서도 알아볼 수 있어야 하며 아이콘 안에 작은 글자나 앱 이름을 넣지 않는다. +시뮬레이터에서는 다음을 확인한다. -파일 기준은 1024 곱하기 1024 PNG이며 투명 배경을 사용하지 않는다. 모서리는 이미지에서 직접 둥글게 만들지 않고 시스템 마스크에 맡긴다. +- 로그인 전 약관과 필수 동의 +- 필터 거절과 정상 게시 +- 신고 시트와 사용자 차단 +- 차단 직후 공개 리캡과 댓글과 지도 핀 숨김 +- 지도와 음악 추천과 플레이리스트 이미지 +- 리캡 생성과 로그 상세 +- 작은 iPhone과 큰 iPhone의 글자 잘림과 버튼 겹침 -변경 후 다음 작업을 수행한다. +Apple이 실제 기기 녹화를 요구했으므로 최종 TestFlight 빌드를 설치한 실제 iPhone에서 다음 흐름을 한 번에 녹화한다. -- `assets/icon.png`를 최종 원본으로 교체한다. -- 스플래시와 Android 아이콘이 같은 브랜드로 보이도록 관련 자산을 정리한다. -- Expo 네이티브 자산을 다시 생성하고 `ios/Soundlog/Images.xcassets/AppIcon.appiconset`을 확인한다. -- 기본 Expo 아이콘 해시를 금지 목록에 넣어 `check:store-release`가 다시 통과시키지 못하게 한다. -- 실제 기기에 설치하여 홈 화면과 설정과 TestFlight에서 같은 아이콘이 보이는지 확인한다. - -아이콘 교체는 기존 빌드의 메타데이터만 수정해서 해결할 수 없다. 반드시 새 빌드를 올린다. - -### 4.9 App Store 스크린샷 교체 - -제출 빌드 10은 iPhone과 iPad를 모두 지원하는 바이너리로 처리되어 있다. 현재 소스의 `supportsTablet: false`와 iOS 대상 기기 설정을 반영한 새 빌드를 올린 뒤 App Store Connect의 기기 제품군이 iPhone으로만 표시되는지 먼저 확인한다. 확인 후 미디어 관리자에서 잘못 등록된 13인치 iPad 스크린샷을 제거한다. +1. 로그아웃 상태에서 약관을 연다. +2. 불쾌한 콘텐츠와 악용 사용자를 용납하지 않는 문구를 보여준다. +3. 필수 동의 전에는 계정 생성이 되지 않음을 보여준다. +4. 심사용 계정으로 로그인한다. +5. 금지 문구가 게시 전에 거절되는 모습을 보여준다. +6. 미리 작성된 다른 사용자의 콘텐츠를 신고한다. +7. 같은 사용자를 차단한다. +8. 해당 사용자의 콘텐츠가 피드와 지도에서 즉시 사라지는 모습을 보여준다. -iPhone 스크린샷은 실제 iOS 앱 화면으로 다시 촬영한다. 현재 iPhone 이미지 다섯 장 중 첫 이미지도 온보딩 화면이므로 모든 이미지를 새 촬영본으로 교체한다. 온보딩과 로그인 화면은 핵심 스크린샷 묶음에서 제외한다. +### 4.7 App Store 스크린샷 재촬영 -권장 순서는 다음과 같다. +최종 서버와 최종 TestFlight 빌드로 다음 순서의 iPhone 이미지를 새로 촬영한다. 우선 최신 6.9인치 규격인 `1320 × 2868` 또는 App Store Connect가 허용하는 다른 6.9인치 규격을 사용한다. 필요하면 6.5인치 세트도 같은 장면과 순서로 만든다. -1. 현재 장소와 여행 모드가 표시된 지도 +1. 현재 장소와 주변 콘텐츠와 여행 시작이 보이는 지도 2. 장소와 무드를 반영한 음악 추천 -3. 플레이리스트 상세와 실제 곡 목록 -4. 사진과 음악으로 리캡을 만드는 화면 +3. 실제 앨범 커버가 있는 플레이리스트 곡 목록 +4. 사진과 음악과 장소로 리캡을 만드는 화면 5. 저장된 여행 로그와 리캡 상세 -스크린샷에는 개발자 도구와 테스트 버튼과 빈 데이터와 반복 폴백 결과가 보이면 안 된다. 서버와 추천과 관광 이미지가 정상 동작하는 최종 배포 환경에서 촬영한다. 원본 앱 화면이 대부분을 차지하게 하고 장식용 목업이 실제 인터페이스를 가리지 않게 한다. - -### 4.10 새 빌드 검증 - -새 빌드는 `1.0.0`의 다음 빌드 번호로 생성한다. 빌드 번호는 App Store Connect의 처리 결과를 확인한 뒤 확정한다. +모든 이미지는 실제 앱 인터페이스가 대부분을 차지해야 한다. 온보딩과 로그인과 개발자 도구와 빈 상태는 제외한다. 단색 커버와 반복 이미지와 동일한 더미 문구가 보이면 다시 촬영한다. 설치 시트에는 처음 세 장만 사용되므로 지도와 음악 추천과 실제 앨범 커버 곡 목록을 앞에 둔다. -프론트엔드 검증은 다음을 포함한다. +신고 화면과 차단 화면은 스토어의 핵심 마케팅 이미지가 아니라 심사 정보에 첨부할 실제 기기 영상과 보조 캡처로 사용한다. -- `npm run typecheck` -- `npm test` -- `npm run doctor` -- `npm run check:store-release` -- iOS 시뮬레이터에서 회원가입 약관과 필터와 신고와 차단과 공개 콘텐츠 숨김을 검증한다. -- 작은 iPhone과 큰 iPhone 화면에서 핵심 화면을 검증한다. +### 4.8 새 빌드와 TestFlight -서버 검증은 다음을 포함한다. +출시 후보 커밋에서 새 iOS production 빌드를 만든다. 빌드가 끝나면 IPA와 TestFlight에서 다음을 확인한다. -- `pnpm typecheck` -- `pnpm test:api` -- `pnpm check:openapi-sync` -- `pnpm build` -- 운영 서버 배포 후 신고와 차단과 필터와 관리자 처리 API를 전용 데모 데이터로 확인한다. +- `UIDeviceFamily`가 iPhone만 포함 +- 앱 아이콘이 최종 LP와 지도 핀 아이콘 +- 앱의 API와 업로드 주소가 `api.soundlog.p-e.kr` +- 개인정보 처리방침과 약관 링크가 실제 200 주소 +- TestFlight 심사용 계정 로그인과 UGC 검증 성공 -웹 화면은 Soundlog 제품 검수 증거로 사용하지 않는다. +### 4.9 App Store Connect 수정과 재제출 -### 4.11 실제 기기 심사 녹화 +모든 검증이 끝난 뒤 다음 순서로 App Store Connect를 수정한다. -Apple이 실제 기기 녹화를 요청했으므로 시뮬레이터 녹화만 첨부해서는 안 된다. TestFlight로 설치한 실제 iPhone에서 하나의 연속된 영상으로 다음 흐름을 보여준다. +1. 새 최종 빌드를 앱 버전 `1.0.0`에 선택한다. +2. 새 빌드가 iPhone 전용으로 표시되는지 확인한다. +3. 선택된 빌드 행과 포함된 자산과 앱 메뉴에서 최종 LP와 지도 핀 아이콘을 확인한다. +4. 모든 iPad 스크린샷이 0개인지 다시 확인한다. +5. 6.9인치 iPhone 스크린샷을 새 핵심 기능 이미지로 등록하고 6.5인치 이미지 사용 범위를 정리한다. +6. 개인정보 처리방침과 지원 URL을 실제 200 응답 주소로 바꾼다. +7. 심사 계정 암호와 연락처 전화번호와 이메일을 유효한 값으로 다시 입력하고 저장한다. +8. 앱 심사 정보에 심사용 계정과 기능 진입 경로와 24시간 운영 절차를 적는다. +9. 실제 기기 녹화본을 첨부한다. +10. `심사 업데이트`를 반영한 뒤 Apple 메시지에 지침별 해결 내용을 답변한다. +11. `앱 심사에 다시 제출` 버튼이 활성화된 것을 확인하고 사용자 승인 후 제출한다. -1. 로그아웃 상태에서 이용약관을 연다. -2. 불쾌한 콘텐츠와 악용 사용자를 용납하지 않는다는 문구를 보여준다. -3. 필수 동의 전에는 회원가입할 수 없음을 보여준다. -4. 심사용 계정으로 로그인한다. -5. 금지된 댓글이 게시 전에 거절되는 모습을 보여준다. -6. 정상 댓글이 게시되는 모습을 보여준다. -7. 다른 사용자의 공개 콘텐츠를 신고한다. -8. 같은 사용자를 차단한다. -9. 차단 즉시 해당 사용자의 콘텐츠가 피드와 지도에서 사라지는 모습을 보여준다. +이미지 삭제와 업로드와 Apple 회신과 최종 재제출은 외부 상태를 변경한다. 실제 실행 직전에 사용자 확인을 받고 진행한다. -영상은 App Store Connect의 앱 심사 정보 메모에 첨부하고 심사 메모에는 각 기능의 정확한 진입 경로를 적는다. +## 5. 최종 심사 메모에 포함할 정보 -### 4.12 재제출 순서 +심사 메모에는 다음 내용을 짧고 재현 가능하게 적는다. -1. 서버의 필터와 신고 운영 기능을 배포한다. -2. 프론트엔드 안전 기능과 최종 아이콘을 포함한 새 iOS 빌드를 만든다. -3. TestFlight에서 실제 기기 검증을 완료한다. -4. 실제 기기 녹화본을 준비한다. -5. App Store Connect에서 새 빌드를 선택한다. -6. iPhone 스크린샷을 교체하고 잘못된 iPad 스크린샷을 제거한다. -7. 심사 메모에 데모 계정과 기능 경로와 운영 정책과 녹화본을 등록한다. -8. Apple 메시지에 각 지적 사항의 해결 내용을 답변한다. -9. 마지막으로 앱 심사에 다시 제출한다. +- 약관 경로와 필수 동의 방법 +- 금지 콘텐츠 필터를 확인하는 입력 예시 +- 공개 리캡 신고 경로 +- 사용자 차단 경로와 즉시 숨김 결과 +- 심사용 계정에 미리 작성된 콘텐츠가 보이는 위치 +- 신고 접수 후 24시간 안에 콘텐츠 삭제와 사용자 제재를 수행하는 운영 절차 +- 공개 개인정보 처리방침과 약관과 고객지원 주소 +- 첨부한 실제 기기 영상에서 각 기능이 나오는 시점 -업로드와 메시지 전송과 재제출은 외부 상태를 변경하므로 실행 직전에 사용자 확인을 받는다. +비밀번호는 문서와 저장소와 대화에 적지 않고 App Store Connect의 전용 암호 필드에만 등록한다. -## 5. 완료 판정표 +## 6. 완료 판정표 | 확인 항목 | 완료 증거 | | --- | --- | -| 약관 | 로그인 전 약관 화면과 필수 동의 차단 영상과 서버의 약관 버전 저장 값 | +| App Store 원본 파악 | 첨부 이미지 세 장과 현재 빌드와 모든 기기 크기 스크린샷을 확인한 기록 | +| 최신 서버 배포 | 운영 OpenAPI에 관리자 신고 처리 경로가 있고 인증 없는 요청이 404가 아닌 인증 실패를 반환 | +| 공개 약관 | 개인정보 처리방침과 약관과 지원 페이지가 HTTPS 200을 반환 | +| 지원 채널 | 외부 주소의 시험 메일을 받고 답장한 기록 | +| 약관 | 로그인 전 약관과 필수 동의 차단 영상과 서버의 약관 동의 값 | | 콘텐츠 필터 | 금지 문구가 저장되지 않는 API 테스트와 실제 기기 화면 | -| 신고 | 콘텐츠별 신고 UI와 서버 신고 레코드와 운영 알림 | -| 차단 | 차단 직후 UI 삭제와 이후 조회 API 제외 테스트 | -| 24시간 처리 | 관리자 신고 큐와 콘텐츠 삭제와 사용자 정지 기능과 기한 알림 | -| 지원 연락처 | 공개 지원 페이지 200 응답과 외부 주소에서 보낸 시험 메일의 수신 및 답장 기록 | -| 심사용 데이터 | 데모 계정 로그인 후 신고 가능한 콘텐츠가 보이는 실제 기기 영상 | -| 앱 아이콘 | 최종 1024 PNG와 새 빌드의 홈 화면과 TestFlight 아이콘 | -| 스크린샷 | 미디어 관리자에 실제 핵심 기능 중심의 iPhone 이미지가 등록된 화면 | -| iPad 범위 | 새 빌드가 iPhone 전용으로 표시되고 잘못된 iPad 이미지가 제거된 화면 | -| 재제출 | 새 빌드 번호와 Apple 회신과 제출 상태가 심사 대기로 변경된 화면 | - -## 6. 계획 자체 검토 결과 - -현재 있는 신고와 차단 API를 그대로 두고 UI만 보여주는 계획은 부족하다. 공개 리캡과 댓글까지 범위를 넓히고 서버가 차단 사용자의 콘텐츠를 모든 조회에서 제외하는지 검증해야 한다. - -금지어 목록만으로 사진과 우회 문구를 완전히 판정할 수 없으므로 공개 사진에는 검토 상태가 필요하다. 자동 이미지 검토 공급자를 바로 정하지 못하더라도 검토 전 콘텐츠를 공개하지 않는 원칙은 유지한다. - -13인치 iPad 스크린샷을 새로 만드는 것은 현재 iPhone 전용 제품 설정과 맞지 않는다. 먼저 새 빌드의 지원 기기 정보를 확인하고 iPad 미디어를 제거하는 방향이 일관된다. - -심사 메시지에 답변만 보내거나 기존 빌드를 다시 제출해서는 아이콘과 안전 기능이 바뀌지 않는다. 세 지적 사항을 모두 반영한 새 빌드와 새 메타데이터를 한 번에 제출해야 한다. +| 신고 | 콘텐츠별 신고 UI와 운영 신고 레코드와 알림 | +| 차단 | 차단 직후 화면 제거와 이후 조회 API 제외 테스트 | +| 24시간 처리 | 관리자 처리와 콘텐츠 숨김과 사용자 정지와 기한 점검 결과 | +| 심사용 데이터 | TestFlight 심사용 계정에서 신고 가능한 다른 사용자 콘텐츠가 보이는 화면 | +| 앱 아이콘 | 최종 IPA와 TestFlight와 홈 화면의 동일 아이콘 | +| iPhone 전용 | 최종 IPA의 `UIDeviceFamily = [1]`과 App Store Connect의 기기 범위 | +| 스크린샷 | 핵심 기능 중심의 새 iPhone 이미지 다섯 장과 iPad 이미지 제거 상태 | +| 실제 기기 영상 | 약관과 필터와 신고와 차단을 연속으로 보여주는 TestFlight 녹화본 | +| 심사 연락처 | 심사 계정 암호와 연락처 전화번호와 이메일 입력 여부 및 실제 로그인 확인 | +| App Store URL | 개인정보 처리방침과 지원 URL이 `api.soundlog.p-e.kr`의 200 응답 주소로 저장된 화면 | +| 재제출 | 새 빌드 번호가 선택되고 Apple 회신이 전송되며 상태가 심사 대기로 변경된 화면 | + +## 7. 현재 즉시 해결해야 하는 항목 + +1. 서버 배포 워크플로 복구와 최신 서버 운영 배포 +2. 공개 약관과 개인정보 처리방침과 지원 URL을 `api.soundlog.p-e.kr`로 전환 +3. 실제 수신 가능한 지원 이메일 확정 +4. 운영 심사용 시드와 UGC 전체 흐름 검증 +5. 현재 앱 UI 변경을 포함한 새 최종 빌드 생성 +6. 실제 기기 영상과 6.9인치 핵심 기능 스크린샷 재촬영 +7. 심사 암호와 연락처 정보와 실제 기기 영상 첨부 보완 +8. App Store Connect 빌드와 아이콘과 메타데이터 교체 +9. 사용자 최종 확인 후 Apple 회신과 재제출 diff --git a/eas.json b/eas.json index 207ab8c..52c80b0 100644 --- a/eas.json +++ b/eas.json @@ -26,9 +26,9 @@ "env": { "EXPO_PUBLIC_SOUNDLOG_API_BASE_URL": "https://api.soundlog.p-e.kr", "EXPO_PUBLIC_SOUNDLOG_API_SOURCE": "server", - "EXPO_PUBLIC_SOUNDLOG_PRIVACY_URL": "https://api.soundlog.shop/legal/privacy", + "EXPO_PUBLIC_SOUNDLOG_PRIVACY_URL": "https://api.soundlog.p-e.kr/legal/privacy", "EXPO_PUBLIC_SOUNDLOG_SUPPORT_EMAIL": "support@soundlog.shop", - "EXPO_PUBLIC_SOUNDLOG_TERMS_URL": "https://api.soundlog.shop/legal/terms", + "EXPO_PUBLIC_SOUNDLOG_TERMS_URL": "https://api.soundlog.p-e.kr/legal/terms", "EXPO_PUBLIC_SOUNDLOG_UPLOAD_ORIGIN": "https://api.soundlog.p-e.kr" } } diff --git a/scripts/check-store-release.js b/scripts/check-store-release.js index 7787e7a..c6c403f 100644 --- a/scripts/check-store-release.js +++ b/scripts/check-store-release.js @@ -112,7 +112,11 @@ function assertProductionEnv(productionEnv) { addError('EAS production env must set EXPO_PUBLIC_SOUNDLOG_TERMS_URL to an HTTPS URL.'); } - if (!supportEmail || supportEmail.endsWith('@example.com')) { + if ( + !/^\S+@\S+\.\S+$/.test(supportEmail ?? '') || + supportEmail.endsWith('@example.com') || + supportEmail.endsWith('@soundlog.shop') + ) { addError('EAS production env must set EXPO_PUBLIC_SOUNDLOG_SUPPORT_EMAIL to a real mailbox.'); } @@ -124,12 +128,12 @@ function assertProductionEnv(productionEnv) { addError('EAS production uploads must use the current Soundlog API origin directly.'); } - if (privacyUrl !== 'https://api.soundlog.shop/legal/privacy') { - addError('EAS production privacy URL must stay on the existing legal-document origin.'); + if (privacyUrl !== 'https://api.soundlog.p-e.kr/legal/privacy') { + addError('EAS production privacy URL must use the current Soundlog API origin.'); } - if (termsUrl !== 'https://api.soundlog.shop/legal/terms') { - addError('EAS production terms URL must stay on the existing legal-document origin.'); + if (termsUrl !== 'https://api.soundlog.p-e.kr/legal/terms') { + addError('EAS production terms URL must use the current Soundlog API origin.'); } } diff --git a/src/components/EmptyState.tsx b/src/components/EmptyState.tsx index 3349183..3e67990 100644 --- a/src/components/EmptyState.tsx +++ b/src/components/EmptyState.tsx @@ -21,7 +21,7 @@ export function EmptyState({ action, description, title }: EmptyStateProps) { ) : null} {action ? ( - + {action.label} ) : null} diff --git a/src/components/MiniPlayer.tsx b/src/components/MiniPlayer.tsx index c6ba305..4fb7768 100644 --- a/src/components/MiniPlayer.tsx +++ b/src/components/MiniPlayer.tsx @@ -497,10 +497,10 @@ export function MiniPlayer() { - + 이 곡으로 기록 diff --git a/src/components/dev/DevTestManager.tsx b/src/components/dev/DevTestManager.tsx index 26dbd2c..f283f36 100644 --- a/src/components/dev/DevTestManager.tsx +++ b/src/components/dev/DevTestManager.tsx @@ -121,16 +121,16 @@ function ManagerButton({ onPress, }: ManagerButtonProps) { const backgroundColor = active - ? '#ffffff' + ? '#4A1D96' : destructive ? 'rgba(248,113,113,0.16)' : 'rgba(255,255,255,0.1)'; const borderColor = active - ? '#ffffff' + ? '#8B72FF' : destructive ? 'rgba(248,113,113,0.28)' : 'rgba(255,255,255,0.12)'; - const color = active ? '#4A1D96' : destructive ? '#fecaca' : 'rgba(255,255,255,0.78)'; + const color = active ? '#FFFFFF' : destructive ? '#fecaca' : 'rgba(255,255,255,0.78)'; return ( - + {isOpeningPlaylist ? '여는 중' : trackMeta} @@ -158,7 +158,7 @@ export function CurrentSoundtrackCard({ {placeTitle} · {placeCaption} diff --git a/src/components/home/HomeSoundtrackBottomSheet.tsx b/src/components/home/HomeSoundtrackBottomSheet.tsx index 7577a2a..f3bf55b 100644 --- a/src/components/home/HomeSoundtrackBottomSheet.tsx +++ b/src/components/home/HomeSoundtrackBottomSheet.tsx @@ -306,7 +306,7 @@ export function HomeSoundtrackBottomSheet({ diff --git a/src/components/home/MusicLogCard.tsx b/src/components/home/MusicLogCard.tsx index 2347a7d..9835614 100644 --- a/src/components/home/MusicLogCard.tsx +++ b/src/components/home/MusicLogCard.tsx @@ -38,7 +38,7 @@ export function MusicLogCard({ accessibilityLabel={`${item.placeName}에서 들은 ${item.trackTitle} 리캡 열기`} accessibilityRole={onPress ? 'button' : undefined} className={`justify-end overflow-hidden rounded-[18px] p-3 ${ - hasImage ? 'bg-black/40' : 'bg-[#f4f4f4]' + hasImage ? 'bg-black/40' : 'bg-[#251A4A]' }`} disabled={!onPress} onPress={onPress} @@ -69,13 +69,13 @@ export function MusicLogCard({ {item.placeName} {item.trackTitle} diff --git a/src/components/home/TravelModeHomeView.tsx b/src/components/home/TravelModeHomeView.tsx index a6d1d26..0dc7d3f 100644 --- a/src/components/home/TravelModeHomeView.tsx +++ b/src/components/home/TravelModeHomeView.tsx @@ -306,10 +306,10 @@ function TravelStartPanel({ - + 여행 시작 @@ -383,7 +383,7 @@ function ActiveRecommendationPanel({ - + 여행 기록 중 @@ -438,13 +438,13 @@ function ActiveRecommendationPanel({ @@ -490,10 +490,10 @@ function ActiveRecommendationPanel({ - + 여행 종료 diff --git a/src/components/home/TravelModeSuggestionSheet.tsx b/src/components/home/TravelModeSuggestionSheet.tsx index 0030f73..40bae92 100644 --- a/src/components/home/TravelModeSuggestionSheet.tsx +++ b/src/components/home/TravelModeSuggestionSheet.tsx @@ -52,9 +52,9 @@ export function TravelModeSuggestionSheet({ accessibilityRole="button" className="h-12 flex-1 items-center justify-center rounded-full" onPress={onStartTravelMode} - style={{ backgroundColor: '#F5A451' }} + style={{ backgroundColor: '#4A1D96' }} > - + Travel Mode 시작 diff --git a/src/components/library/LibraryScreen.tsx b/src/components/library/LibraryScreen.tsx index a4cc914..9a2b6ed 100644 --- a/src/components/library/LibraryScreen.tsx +++ b/src/components/library/LibraryScreen.tsx @@ -272,13 +272,13 @@ export function LibraryScreen() { className="h-10 flex-1 items-center justify-center rounded-full" onPress={() => selectTab(tab.id)} style={{ - backgroundColor: isSelected ? "#ffffff" : "transparent", + backgroundColor: isSelected ? "#4A1D96" : "transparent", }} > {tab.label} diff --git a/src/components/moderation/ReportContentSheet.tsx b/src/components/moderation/ReportContentSheet.tsx index 504f38f..089e405 100644 --- a/src/components/moderation/ReportContentSheet.tsx +++ b/src/components/moderation/ReportContentSheet.tsx @@ -156,7 +156,7 @@ export function ReportContentSheet({ void handleSubmit()} style={{ opacity: isSubmitting || !target ? 0.55 : 1 }} diff --git a/src/components/moment-capture/CameraPermissionState.tsx b/src/components/moment-capture/CameraPermissionState.tsx index c844ead..e3b4ca8 100644 --- a/src/components/moment-capture/CameraPermissionState.tsx +++ b/src/components/moment-capture/CameraPermissionState.tsx @@ -40,7 +40,7 @@ export function CameraPermissionState({ void Linking.openSettings() : onRequest} > diff --git a/src/components/moment-capture/MomentCaptureScreen.tsx b/src/components/moment-capture/MomentCaptureScreen.tsx index 2a2fe72..4c74843 100644 --- a/src/components/moment-capture/MomentCaptureScreen.tsx +++ b/src/components/moment-capture/MomentCaptureScreen.tsx @@ -402,7 +402,7 @@ export function MomentCaptureScreen() { prepareReview()} > diff --git a/src/components/moment-capture/MomentPhotoCanvas.tsx b/src/components/moment-capture/MomentPhotoCanvas.tsx index 3fb47ea..ca3ccab 100644 --- a/src/components/moment-capture/MomentPhotoCanvas.tsx +++ b/src/components/moment-capture/MomentPhotoCanvas.tsx @@ -970,14 +970,14 @@ function StickerSegment({ key={option.value} onPress={() => onChange(option.value)} style={{ - backgroundColor: selected ? '#fff' : 'transparent', + backgroundColor: selected ? '#4A1D96' : 'transparent', opacity: disabled ? 0.5 : 1, }} > {option.label} @@ -1062,30 +1062,30 @@ function getStickerThemeStyle(theme: StickerTheme) { if (theme === 'lime') { return { container: { - backgroundColor: 'rgba(183,230,40,0.94)', - borderColor: 'rgba(255,255,255,0.42)', + backgroundColor: 'rgba(45,56,12,0.94)', + borderColor: 'rgba(183,230,40,0.72)', }, - dateText: { color: 'rgba(74,29,150,0.82)' }, - divider: { backgroundColor: 'rgba(5,9,22,0.28)' }, - iconBubble: { backgroundColor: 'rgba(5,9,22,0.13)' }, - iconColor: 'rgba(74,29,150,0.9)', - metaText: { color: 'rgba(74,29,150,0.78)' }, - timeText: { color: '#4A1D96' }, + dateText: { color: 'rgba(255,255,255,0.82)' }, + divider: { backgroundColor: 'rgba(255,255,255,0.28)' }, + iconBubble: { backgroundColor: 'rgba(255,255,255,0.13)' }, + iconColor: 'rgba(255,255,255,0.9)', + metaText: { color: 'rgba(255,255,255,0.78)' }, + timeText: { color: '#FFFFFF' }, }; } if (theme === 'mono') { return { container: { - backgroundColor: 'rgba(255,255,255,0.92)', + backgroundColor: 'rgba(5,9,22,0.92)', borderColor: 'rgba(255,255,255,0.66)', }, - dateText: { color: 'rgba(74,29,150,0.76)' }, - divider: { backgroundColor: 'rgba(5,9,22,0.22)' }, - iconBubble: { backgroundColor: 'rgba(5,9,22,0.08)' }, - iconColor: 'rgba(74,29,150,0.82)', - metaText: { color: 'rgba(74,29,150,0.68)' }, - timeText: { color: '#4A1D96' }, + dateText: { color: 'rgba(255,255,255,0.76)' }, + divider: { backgroundColor: 'rgba(255,255,255,0.22)' }, + iconBubble: { backgroundColor: 'rgba(255,255,255,0.08)' }, + iconColor: 'rgba(255,255,255,0.82)', + metaText: { color: 'rgba(255,255,255,0.68)' }, + timeText: { color: '#FFFFFF' }, }; } diff --git a/src/components/moment-capture/MomentReviewPanel.tsx b/src/components/moment-capture/MomentReviewPanel.tsx index e801b27..bb89262 100644 --- a/src/components/moment-capture/MomentReviewPanel.tsx +++ b/src/components/moment-capture/MomentReviewPanel.tsx @@ -189,7 +189,7 @@ export const MomentReviewPanel = forwardRef< 사진 없이 리캡을 만들어요 - + 장소, 음악과 무드만으로도 저장할 수 있어요. @@ -232,7 +232,7 @@ export const MomentReviewPanel = forwardRef< accessibilityRole="button" accessibilityState={{ disabled, selected }} className={`min-h-11 flex-1 items-center justify-center rounded-full ${ - selected ? "bg-soundlog-lime" : "bg-transparent" + selected ? "bg-soundlog-selected" : "bg-transparent" }`} disabled={disabled || isSaving} key={option.value} @@ -310,7 +310,7 @@ export const MomentReviewPanel = forwardRef< accessibilityState={{ selected }} className={`min-h-11 justify-center rounded-full border px-4 ${ selected - ? "border-soundlog-lime bg-soundlog-lime" + ? "border-soundlog-lime bg-soundlog-selected" : "border-white/10 bg-white/[0.06]" }`} disabled={isSaving} @@ -337,13 +337,13 @@ export const MomentReviewPanel = forwardRef< accessibilityLabel="이 리캡 저장하기" accessibilityRole="button" accessibilityState={{ busy: isSaving, disabled: isSaving }} - className="h-14 items-center justify-center rounded-xl bg-soundlog-lime" + className="h-14 items-center justify-center rounded-xl border border-soundlog-lime/45 bg-soundlog-action" disabled={isSaving} onPress={onSave} style={{ opacity: isSaving ? 0.72 : 1 }} > {isSaving ? ( - + ) : ( 이 리캡 저장하기 diff --git a/src/components/my/RecapLogGuide.tsx b/src/components/my/RecapLogGuide.tsx index b202935..2d21c8b 100644 --- a/src/components/my/RecapLogGuide.tsx +++ b/src/components/my/RecapLogGuide.tsx @@ -31,7 +31,7 @@ function GuideRow({ description, icon, label, title }: GuideRowProps) { {title} - + {description} diff --git a/src/components/onboarding/OnboardingScreen.tsx b/src/components/onboarding/OnboardingScreen.tsx index f94a308..3044f77 100644 --- a/src/components/onboarding/OnboardingScreen.tsx +++ b/src/components/onboarding/OnboardingScreen.tsx @@ -314,7 +314,7 @@ export function OnboardingScreen() { @@ -384,7 +384,7 @@ export function OnboardingScreen() { accessibilityState={{ selected }} className={`min-h-11 justify-center rounded-full border px-5 ${ selected - ? 'border-soundlog-lime bg-soundlog-lime' + ? 'border-soundlog-lime bg-soundlog-selected' : 'border-white/10 bg-white/[0.06]' }`} key={option.label} @@ -415,7 +415,7 @@ export function OnboardingScreen() { accessibilityState={{ selected }} className={`min-h-11 justify-center rounded-full border px-5 ${ selected - ? 'border-soundlog-lime bg-soundlog-lime' + ? 'border-soundlog-lime bg-soundlog-selected' : 'border-white/10 bg-white/[0.06]' }`} key={mood} @@ -463,7 +463,7 @@ export function OnboardingScreen() { handlePrimarySetup(locationRecommendationEnabled)} style={{ opacity: isSaving ? 0.55 : 1 }} diff --git a/src/components/playlist/PlaylistMusicFilter.tsx b/src/components/playlist/PlaylistMusicFilter.tsx index a12bd85..7a8dc46 100644 --- a/src/components/playlist/PlaylistMusicFilter.tsx +++ b/src/components/playlist/PlaylistMusicFilter.tsx @@ -34,7 +34,7 @@ export function PlaylistMusicFilter({ key={filter} onPress={() => onSelectMoodFilter(filter)} style={{ - backgroundColor: isSelected ? '#B7E628' : 'rgba(255, 255, 255, 0.09)', + backgroundColor: isSelected ? '#4A1D96' : 'rgba(255, 255, 255, 0.09)', borderColor: isSelected ? '#B7E628' : 'rgba(255, 255, 255, 0.22)', }} > diff --git a/src/components/playlist/PlaylistState.tsx b/src/components/playlist/PlaylistState.tsx index 7f854c3..92343e3 100644 --- a/src/components/playlist/PlaylistState.tsx +++ b/src/components/playlist/PlaylistState.tsx @@ -35,7 +35,7 @@ export function PlaylistErrorState({ onRetry }: PlaylistErrorStateProps) { {onRetry ? ( diff --git a/src/components/recap-share/RecapPreviewCard.tsx b/src/components/recap-share/RecapPreviewCard.tsx index 79b70c7..49b5835 100644 --- a/src/components/recap-share/RecapPreviewCard.tsx +++ b/src/components/recap-share/RecapPreviewCard.tsx @@ -518,7 +518,7 @@ function RecapMapTemplate({ recap }: { recap: RecapShare }) { {recap.placeName} - + {hasRecordedLocations ? routeSegments.length && recap.routePoints?.length ? `${recap.routePoints.length}개 이동 좌표 저장됨` @@ -534,7 +534,7 @@ function RecapMapTemplate({ recap }: { recap: RecapShare }) { key={moment.id} style={pinPositions[index % pinPositions.length]} > - + {index + 1} @@ -616,28 +616,28 @@ function getRecapMusicStickerThemeStyle(theme: RecapMusicStickerTheme) { if (theme === 'lime') { return { container: { - backgroundColor: 'rgba(183,230,40,0.94)', - borderColor: 'rgba(255,255,255,0.42)', + backgroundColor: 'rgba(45,56,12,0.94)', + borderColor: 'rgba(183,230,40,0.72)', }, - iconBubble: { backgroundColor: 'rgba(5,9,22,0.14)' }, - iconColor: '#4A1D96', - metaText: { color: 'rgba(74,29,150,0.76)' }, - primaryText: { color: '#4A1D96' }, - secondaryText: { color: 'rgba(74,29,150,0.82)' }, + iconBubble: { backgroundColor: 'rgba(255,255,255,0.14)' }, + iconColor: '#FFFFFF', + metaText: { color: 'rgba(255,255,255,0.76)' }, + primaryText: { color: '#FFFFFF' }, + secondaryText: { color: 'rgba(255,255,255,0.82)' }, }; } if (theme === 'mono') { return { container: { - backgroundColor: 'rgba(255,255,255,0.92)', + backgroundColor: 'rgba(5,9,22,0.92)', borderColor: 'rgba(255,255,255,0.66)', }, - iconBubble: { backgroundColor: 'rgba(5,9,22,0.08)' }, - iconColor: 'rgba(74,29,150,0.84)', - metaText: { color: 'rgba(74,29,150,0.7)' }, - primaryText: { color: '#4A1D96' }, - secondaryText: { color: 'rgba(74,29,150,0.76)' }, + iconBubble: { backgroundColor: 'rgba(255,255,255,0.08)' }, + iconColor: 'rgba(255,255,255,0.84)', + metaText: { color: 'rgba(255,255,255,0.7)' }, + primaryText: { color: '#FFFFFF' }, + secondaryText: { color: 'rgba(255,255,255,0.76)' }, }; } diff --git a/src/components/recap-share/RecapRouteMap.tsx b/src/components/recap-share/RecapRouteMap.tsx index 0713bb6..3230050 100644 --- a/src/components/recap-share/RecapRouteMap.tsx +++ b/src/components/recap-share/RecapRouteMap.tsx @@ -358,7 +358,7 @@ export function RecapRouteMap({ recap }: { recap: RecapShare }) { key={group.moments.map((moment) => moment.id).join(":")} onPress={() => setSelectedMoments(group.moments)} > - + {group.moments.length > 1 ? group.moments.length : index + 1} diff --git a/src/components/recap-share/RecapShareScreen.tsx b/src/components/recap-share/RecapShareScreen.tsx index 5a862af..6ac6bb6 100644 --- a/src/components/recap-share/RecapShareScreen.tsx +++ b/src/components/recap-share/RecapShareScreen.tsx @@ -273,7 +273,7 @@ export function RecapShareScreen({ recapId }: RecapShareScreenProps) { selected, }} className={`min-h-[38px] min-w-[92px] items-center justify-center rounded-full px-4 ${ - selected ? "bg-soundlog-lime" : "bg-transparent" + selected ? "bg-soundlog-selected" : "bg-transparent" }`} disabled={isUpdatingVisibility} key={option} diff --git a/src/components/recap-share/RecapShareState.tsx b/src/components/recap-share/RecapShareState.tsx index 32ef945..b0d5014 100644 --- a/src/components/recap-share/RecapShareState.tsx +++ b/src/components/recap-share/RecapShareState.tsx @@ -32,7 +32,7 @@ export function RecapShareErrorState({ onRetry }: RecapShareErrorStateProps) { {onRetry ? ( 다시 시도 diff --git a/src/components/recap-share/RecapTemplateSelector.tsx b/src/components/recap-share/RecapTemplateSelector.tsx index ce2255e..8f9eac7 100644 --- a/src/components/recap-share/RecapTemplateSelector.tsx +++ b/src/components/recap-share/RecapTemplateSelector.tsx @@ -33,7 +33,7 @@ export function RecapTemplateSelector({ accessibilityRole="button" accessibilityState={{ selected: isSelected }} className={`min-h-[44px] flex-1 items-center justify-center rounded-full px-1 ${ - isSelected ? 'bg-white' : 'bg-transparent' + isSelected ? 'bg-soundlog-selected' : 'bg-transparent' }`} key={option.id} onPress={() => onSelect(option.id)} diff --git a/src/components/recap-share/RecapTravelSummaryCard.tsx b/src/components/recap-share/RecapTravelSummaryCard.tsx index 68ad391..635015e 100644 --- a/src/components/recap-share/RecapTravelSummaryCard.tsx +++ b/src/components/recap-share/RecapTravelSummaryCard.tsx @@ -61,7 +61,7 @@ export function RecapTravelSummaryCard({ recap }: { recap: RecapShare }) { return ( - + {summary.startPlaceName}에서 {summary.endPlaceName}까지 diff --git a/src/components/recap-share/ShareActionList.tsx b/src/components/recap-share/ShareActionList.tsx index da36e3a..36a2c26 100644 --- a/src/components/recap-share/ShareActionList.tsx +++ b/src/components/recap-share/ShareActionList.tsx @@ -12,13 +12,13 @@ const actions: Array<{ textColor: string; }> = [ { - backgroundColor: '#B7E628', + backgroundColor: '#4A1D96', icon: 'download', - iconColor: '#4A1D96', + iconColor: '#FFFFFF', id: 'save', label: '이미지 저장', loadingLabel: '저장 중', - textColor: '#4A1D96', + textColor: '#FFFFFF', }, { backgroundColor: 'rgba(255,255,255,0.1)', diff --git a/src/components/recap/RecapListScreen.tsx b/src/components/recap/RecapListScreen.tsx index 11a5e8e..035e6d7 100644 --- a/src/components/recap/RecapListScreen.tsx +++ b/src/components/recap/RecapListScreen.tsx @@ -193,7 +193,7 @@ function LogGridCard({ accessibilityRole="button" accessibilityState={{ disabled: isUpdating }} className={`absolute right-2 top-2 rounded-full px-2 py-1 ${ - visibility === "public" ? "bg-soundlog-lime" : "bg-black/48" + visibility === "public" ? "bg-soundlog-selected" : "bg-black/48" }`} disabled={isUpdating} onPress={(event) => onChangeVisibility(entry, nextVisibility, event)} diff --git a/src/components/travel/CommunityRecapCard.tsx b/src/components/travel/CommunityRecapCard.tsx index 5d7ab49..d8dd559 100644 --- a/src/components/travel/CommunityRecapCard.tsx +++ b/src/components/travel/CommunityRecapCard.tsx @@ -184,7 +184,7 @@ function CandidateMomentRow({ accessibilityRole="button" className={` rounded-full px-2.5 py-1 - ${moment.status === "accepted" ? "bg-white/10" : "bg-soundlog-lime"} + ${moment.status === "accepted" ? "bg-white/10" : "bg-soundlog-selected"} `} disabled={pendingStatus} onPress={onToggleStatus} @@ -768,7 +768,7 @@ export function CommunityRecapCard({ ( @@ -960,12 +960,12 @@ export function CommunityRecapCard({ void handleCreateRoom()} style={{ opacity: !canUseRoom || isCreatingRoom ? 0.55 : 1 }} > - + {isCreatingRoom ? "여행방 생성 중" : "새 여행방 만들기"} diff --git a/src/components/travel/EndTravelConfirmModal.tsx b/src/components/travel/EndTravelConfirmModal.tsx index 359e511..c27440c 100644 --- a/src/components/travel/EndTravelConfirmModal.tsx +++ b/src/components/travel/EndTravelConfirmModal.tsx @@ -52,7 +52,7 @@ export function EndTravelConfirmModal({ onSelectMode(mode.value)} @@ -84,7 +84,7 @@ export function TravelModeBottomSheet({ - + 동행자의 사진, 곡과 메모를 모아 하나의 공동 리캡으로 정리해요. {message ? ( - + {message} ) : null} @@ -554,7 +554,7 @@ export function TravelRoomDetailScreen({ diff --git a/src/components/travel/TravelStatusCard.tsx b/src/components/travel/TravelStatusCard.tsx index f884031..ad05130 100644 --- a/src/components/travel/TravelStatusCard.tsx +++ b/src/components/travel/TravelStatusCard.tsx @@ -117,7 +117,7 @@ export function TravelStatusCard({ 여행 종료 @@ -155,7 +155,7 @@ export function TravelStatusCard({ 새 여행 시작 diff --git a/src/components/travel/live-sound-map/LiveSoundMapSection.tsx b/src/components/travel/live-sound-map/LiveSoundMapSection.tsx index c248f18..c6c795b 100644 --- a/src/components/travel/live-sound-map/LiveSoundMapSection.tsx +++ b/src/components/travel/live-sound-map/LiveSoundMapSection.tsx @@ -190,7 +190,7 @@ export function LiveSoundMapSection({ }, }[liveStatus]; const statusBadgeClassName = isLive - ? 'bg-soundlog-lime' + ? 'bg-soundlog-selected' : liveStatus === 'failed' ? 'bg-amber-300/15' : 'bg-white/10'; @@ -644,7 +644,7 @@ export function LiveSoundMapSection({ {statusPillLabel ? ( void handleUpdateMateRequest(request, 'accept')} style={{ opacity: isBusy ? 0.55 : 1 }} @@ -825,7 +825,7 @@ export function LiveSoundMapSection({ {matchModeTabs.map((label, index) => ( @@ -868,7 +868,7 @@ export function LiveSoundMapSection({ {createProfileSummary(match)} · {match.pin.placeName ?? '대략 위치'} · 정확한 좌표 숨김 - + {match.matchScore}% @@ -900,7 +900,7 @@ export function LiveSoundMapSection({ void handleMateRequest(match)} diff --git a/src/components/travel/live-sound-map/SoundMapView.tsx b/src/components/travel/live-sound-map/SoundMapView.tsx index fcbc1be..0d4daf3 100644 --- a/src/components/travel/live-sound-map/SoundMapView.tsx +++ b/src/components/travel/live-sound-map/SoundMapView.tsx @@ -68,7 +68,7 @@ const markerPalette: Record< border: '#4A1D96', icon: '#4A1D96', labelBackground: 'rgba(9,5,21,0.18)', - labelText: '#4A1D96', + labelText: '#FFFFFF', }, nearby: { background: '#2A1A15', diff --git a/src/components/travel/recap-map/RecapMapSection.tsx b/src/components/travel/recap-map/RecapMapSection.tsx index e8bf0a8..366bd38 100644 --- a/src/components/travel/recap-map/RecapMapSection.tsx +++ b/src/components/travel/recap-map/RecapMapSection.tsx @@ -386,7 +386,7 @@ export function RecapMapSection({ diff --git a/src/constants/colors.ts b/src/constants/colors.ts index 4cc6f77..6700b70 100644 --- a/src/constants/colors.ts +++ b/src/constants/colors.ts @@ -19,7 +19,7 @@ export const colors = { card: '#080D18', cardElevated: '#090E1B', chip: '#171B2A', - chipSelected: '#B7E628', + chipSelected: '#4A1D96', player: '#45343D', tab: 'rgba(10, 16, 30, 0.78)', glass: 'rgba(255,255,255,0.1)', @@ -35,11 +35,13 @@ export const colors = { gold: '#B1913A', lime: '#B7E628', warning: '#FF8A3D', + action: '#4A1D96', + danger: '#8E310D', }, text: { primary: '#FFFFFF', - secondary: '#ACACAC', + secondary: 'rgba(255,255,255,0.68)', muted: 'rgba(255,255,255,0.58)', - inverse: '#4A1D96', + inverse: '#FFFFFF', }, } as const; diff --git a/src/constants/legal.ts b/src/constants/legal.ts index 0695ea5..805813f 100644 --- a/src/constants/legal.ts +++ b/src/constants/legal.ts @@ -1,8 +1,11 @@ export const SOUNDLOG_SUPPORT_EMAIL = process.env.EXPO_PUBLIC_SOUNDLOG_SUPPORT_EMAIL ?? 'support@soundlog.shop'; -export const SOUNDLOG_PRIVACY_URL = process.env.EXPO_PUBLIC_SOUNDLOG_PRIVACY_URL; +export const SOUNDLOG_PRIVACY_URL = + process.env.EXPO_PUBLIC_SOUNDLOG_PRIVACY_URL ?? + 'https://api.soundlog.p-e.kr/legal/privacy'; -export const SOUNDLOG_TERMS_URL = process.env.EXPO_PUBLIC_SOUNDLOG_TERMS_URL; +export const SOUNDLOG_TERMS_URL = + process.env.EXPO_PUBLIC_SOUNDLOG_TERMS_URL ?? 'https://api.soundlog.p-e.kr/legal/terms'; export const SOUNDLOG_TERMS_VERSION = '2026-08-15'; diff --git a/src/design-system/primitives.tsx b/src/design-system/primitives.tsx index ee60d79..1ad2558 100644 --- a/src/design-system/primitives.tsx +++ b/src/design-system/primitives.tsx @@ -35,9 +35,9 @@ const buttonSizeClass: Record = { }; const buttonVariantClass: Record = { - danger: 'bg-soundlog-warning', + danger: 'bg-soundlog-danger', ghost: 'border border-white/10 bg-white/10', - primary: 'bg-soundlog-lime', + primary: 'border border-soundlog-lime/45 bg-soundlog-action', secondary: 'border border-white/15 bg-transparent', }; @@ -48,8 +48,8 @@ const buttonTextClass: Record = { secondary: 'text-white', }; -function getButtonIconColor(variant: SoundlogButtonVariant) { - return variant === 'primary' || variant === 'danger' ? '#4A1D96' : '#FFFFFF'; +function getButtonIconColor() { + return '#FFFFFF'; } export function SoundlogButton({ @@ -84,7 +84,7 @@ export function SoundlogButton({ > {iconName ? ( diff --git a/src/design-system/recipes.ts b/src/design-system/recipes.ts index 257949b..3825505 100644 --- a/src/design-system/recipes.ts +++ b/src/design-system/recipes.ts @@ -27,10 +27,10 @@ export const soundlogRecipes = { small: 'min-h-[28px] px-3', }, button: { - primary: 'h-14 items-center justify-center rounded-full bg-soundlog-lime', + primary: 'h-14 items-center justify-center rounded-full border border-soundlog-lime/45 bg-soundlog-action', secondary: 'h-14 items-center justify-center rounded-full border border-white/15', - compactPrimary: 'h-11 items-center justify-center rounded-full bg-soundlog-lime', - compactDanger: 'h-11 items-center justify-center rounded-full bg-soundlog-warning', + compactPrimary: 'h-11 items-center justify-center rounded-full border border-soundlog-lime/45 bg-soundlog-action', + compactDanger: 'h-11 items-center justify-center rounded-full bg-soundlog-danger', icon: 'h-11 w-11 items-center justify-center rounded-full bg-white/10', }, control: { diff --git a/tailwind.config.js b/tailwind.config.js index 523bd69..b75a9f3 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -12,7 +12,7 @@ module.exports = { card: '#080D18', elevated: '#090E1B', chip: '#171B2A', - selected: '#B7E628', + selected: '#4A1D96', border: '#364283', focus: '#B7E628', blue: '#6EA8FF', @@ -21,7 +21,9 @@ module.exports = { lime: '#B7E628', player: '#45343D', warning: '#FF8A3D', - inverse: '#4A1D96', + action: '#4A1D96', + danger: '#8E310D', + inverse: '#FFFFFF', }, }, opacity: { @@ -38,10 +40,13 @@ module.exports = { 45: '0.45', 46: '0.46', 48: '0.48', + 52: '0.52', 55: '0.55', + 56: '0.56', 58: '0.58', 62: '0.62', 65: '0.65', + 66: '0.66', 68: '0.68', 72: '0.72', 74: '0.74', From 349a48bce75a51ef010d98c51dcc697fdc32878c Mon Sep 17 00:00:00 2001 From: manNomi Date: Thu, 20 Aug 2026 23:13:45 +0900 Subject: [PATCH 3/5] =?UTF-8?q?chore:=20Expo=2056=20=ED=8C=A8=EC=B9=98=20?= =?UTF-8?q?=EB=B2=84=EC=A0=84=20=EC=A0=95=ED=95=A9=EC=84=B1=20=EB=B3=B4?= =?UTF-8?q?=EA=B0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package-lock.json | 253 ++++++++++++++++++++++++---------------------- package.json | 21 ++-- 2 files changed, 144 insertions(+), 130 deletions(-) diff --git a/package-lock.json b/package-lock.json index abbfdab..8f5d715 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,22 +11,22 @@ "@expo/vector-icons": "^15.1.1", "@react-native-async-storage/async-storage": "2.2.0", "@tanstack/react-query": "^5.100.14", - "expo": "~56.0.19", - "expo-build-properties": "~56.0.25", + "expo": "~56.0.20", + "expo-build-properties": "~56.0.26", "expo-camera": "~56.0.8", "expo-constants": "~56.0.21", - "expo-dev-client": "~56.0.24", + "expo-dev-client": "~56.0.25", "expo-file-system": "~56.0.7", "expo-font": "~56.0.5", - "expo-image": "~56.0.11", - "expo-image-picker": "~56.0.23", + "expo-image": "~56.0.12", + "expo-image-picker": "~56.0.24", "expo-linear-gradient": "~56.0.4", - "expo-linking": "~56.0.16", - "expo-location": "~56.0.23", - "expo-media-library": "~56.0.10", - "expo-router": "~56.2.18", + "expo-linking": "~56.0.17", + "expo-location": "~56.0.24", + "expo-media-library": "~56.0.11", + "expo-router": "~56.2.19", "expo-secure-store": "~56.0.4", - "expo-sharing": "~56.0.24", + "expo-sharing": "~56.0.25", "expo-status-bar": "~56.0.4", "nativewind": "^4.2.4", "react": "19.2.3", @@ -1289,9 +1289,9 @@ } }, "node_modules/@expo/config-plugins": { - "version": "56.0.14", - "resolved": "https://registry.npmjs.org/@expo/config-plugins/-/config-plugins-56.0.14.tgz", - "integrity": "sha512-pdjBwH67BF1dnim4N/OxgBshhil27eu3C9uuhwzzK2Bnzl6bIcdt9WjEVuMXluFnEpWe0ws5sxE8NvBR5Ms7bQ==", + "version": "56.0.15", + "resolved": "https://registry.npmjs.org/@expo/config-plugins/-/config-plugins-56.0.15.tgz", + "integrity": "sha512-MsSko6coyBWgx7oWFFETxAZZtGJlYykCuCKAGxYgzfT1leUxPVNg1FuGD9moPObDxGadB9l6GJeDuaM2tuCmsQ==", "license": "MIT", "dependencies": { "@expo/config-types": "^56.0.7", @@ -1586,9 +1586,9 @@ } }, "node_modules/@expo/metro-runtime": { - "version": "56.0.19", - "resolved": "https://registry.npmjs.org/@expo/metro-runtime/-/metro-runtime-56.0.19.tgz", - "integrity": "sha512-2YNmRE04fZLhCYhz6A2xuGXW7+ekhrt7ihddSOS9tw3MfC/wNtOZSSufDCxlkbRzCBgbbBCgD9mQ6CkCRgTK2A==", + "version": "56.0.20", + "resolved": "https://registry.npmjs.org/@expo/metro-runtime/-/metro-runtime-56.0.20.tgz", + "integrity": "sha512-ICwOVi5rwcQQn3/euXytnhS4YoU6wenKchCx8HFSvl9vKx0w6Tv4IE9oWE09UT6rTRtmVavRHqfPTM8eaNBLtA==", "license": "MIT", "dependencies": { "@expo/log-box": "^56.0.14", @@ -1658,19 +1658,19 @@ } }, "node_modules/@expo/prebuild-config": { - "version": "56.0.21", - "resolved": "https://registry.npmjs.org/@expo/prebuild-config/-/prebuild-config-56.0.21.tgz", - "integrity": "sha512-HjhcxGsi6OoDSQxxw06tRq27gRXfXLYlsiXj3cH0R4GKiNK4yQAINKUFup4R3DtYxYNde87eHaNX28IMBCm9qA==", + "version": "56.0.22", + "resolved": "https://registry.npmjs.org/@expo/prebuild-config/-/prebuild-config-56.0.22.tgz", + "integrity": "sha512-U0/7zZWFj/6kMSwNHPfl7QGfM3VyoONPC5TjFXh1gyYcdE1sVBZv5pJH5uRC0DgRNqyeiHvU429C5snIAZ5x2w==", "license": "MIT", "dependencies": { "@expo/config": "~56.0.13", - "@expo/config-plugins": "~56.0.14", + "@expo/config-plugins": "~56.0.15", "@expo/config-types": "^56.0.7", "@expo/image-utils": "^0.10.4", "@expo/json-file": "^10.2.0", "@react-native/normalize-colors": "0.85.3", "debug": "^4.3.1", - "expo-modules-autolinking": "~56.0.21", + "expo-modules-autolinking": "~56.0.22", "resolve-from": "^5.0.0", "semver": "^7.6.0" } @@ -1725,9 +1725,9 @@ "license": "MIT" }, "node_modules/@expo/ui": { - "version": "56.0.25", - "resolved": "https://registry.npmjs.org/@expo/ui/-/ui-56.0.25.tgz", - "integrity": "sha512-2k1JK2JXzQPzwNNyplav2RD03Rrmd/7BRz2mfqPYLpg6jSGhcOGO3UABYKuGkxU9RFYSEuAmEkeBaL4eF10E8Q==", + "version": "56.0.26", + "resolved": "https://registry.npmjs.org/@expo/ui/-/ui-56.0.26.tgz", + "integrity": "sha512-FtGq/lDXcf2uo3x7pklvetnSYioriIFe85HMLAl72fsFfcUTkwYEPBDvq0iTI214peDTehuWXYiQi0oyuMsaIw==", "license": "MIT", "dependencies": { "sf-symbols-typescript": "^2.1.0", @@ -3740,9 +3740,9 @@ } }, "node_modules/agent-cli-detector": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/agent-cli-detector/-/agent-cli-detector-0.1.5.tgz", - "integrity": "sha512-6xvLw0EGPuxoYGZeqyMV4AK+WoZ31jsqb7a5pln1BTf6oDFsrgvfCJT7E7y9oAqifJZhJ3fZ0J36H7o6JzrB0Q==", + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/agent-cli-detector/-/agent-cli-detector-0.1.6.tgz", + "integrity": "sha512-vKrPeEVN3upDF3GjWxsWBbwQgMtNJ8VB1cduPvK3svmmz4ENpS7yaPxbogsbe3w+xp9xp2Cu+0Ar41rjAR4+lA==", "license": "MIT", "bin": { "agent-cli-detector": "dist/cli.js" @@ -5026,15 +5026,15 @@ } }, "node_modules/expo": { - "version": "56.0.19", - "resolved": "https://registry.npmjs.org/expo/-/expo-56.0.19.tgz", - "integrity": "sha512-IqsNM8oATJpgAo+sLM90Ct533J+T1grCh8PpvcsrvEVw6vF42VIHKO0DGEgarAio8LOWm7T9eUiJGgh0ouAe3g==", + "version": "56.0.20", + "resolved": "https://registry.npmjs.org/expo/-/expo-56.0.20.tgz", + "integrity": "sha512-PCjEnbVK3gssHpODgt7PqCwrLx3ERfWRFrVyi38P8K/UMHD6mPiehuNE786WPxZ5tx8xUtsshKtuBxaUQMu3dw==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.20.0", - "@expo/cli": "^56.1.23", + "@expo/cli": "^56.1.24", "@expo/config": "~56.0.13", - "@expo/config-plugins": "~56.0.14", + "@expo/config-plugins": "~56.0.15", "@expo/devtools": "~56.0.2", "@expo/dom-webview": "~56.0.6", "@expo/fingerprint": "^0.19.9", @@ -5044,13 +5044,13 @@ "@expo/metro-config": "~56.0.18", "@ungap/structured-clone": "^1.3.0", "babel-preset-expo": "~56.0.19", - "expo-asset": "~56.0.22", - "expo-constants": "~56.0.23", - "expo-file-system": "~56.0.9", + "expo-asset": "~56.0.23", + "expo-constants": "~56.0.24", + "expo-file-system": "~56.0.10", "expo-font": "~56.0.7", "expo-keep-awake": "~56.0.3", - "expo-modules-autolinking": "~56.0.21", - "expo-modules-core": "~56.0.23", + "expo-modules-autolinking": "~56.0.22", + "expo-modules-core": "~56.0.24", "pretty-format": "^29.7.0", "react-refresh": "^0.14.2", "whatwg-url-minimum": "^0.1.2" @@ -5088,13 +5088,13 @@ } }, "node_modules/expo-asset": { - "version": "56.0.22", - "resolved": "https://registry.npmjs.org/expo-asset/-/expo-asset-56.0.22.tgz", - "integrity": "sha512-KIlIskLTD1a/rtoEIiJv0IeRAnnxRavD2oLyHI0aCEs4zl0yNHM2xLZHh05PWvAHJbBlbopfOWW1g8IbJp6ttg==", + "version": "56.0.23", + "resolved": "https://registry.npmjs.org/expo-asset/-/expo-asset-56.0.23.tgz", + "integrity": "sha512-QN0rEr7I6mAx4Hk2wWBdSzeTtt+6hqg2ImMBcL3SoCqHjxBUVJ05+wyb1jAlZOm/R0HkpMZbkGqBnyqUNSJB1w==", "license": "MIT", "dependencies": { "@expo/image-utils": "^0.10.4", - "expo-constants": "~56.0.23" + "expo-constants": "~56.0.24" }, "peerDependencies": { "expo": "*", @@ -5103,9 +5103,9 @@ } }, "node_modules/expo-build-properties": { - "version": "56.0.25", - "resolved": "https://registry.npmjs.org/expo-build-properties/-/expo-build-properties-56.0.25.tgz", - "integrity": "sha512-UzLeBGlLMxDAfMOUuFwaPgqY7jxZPT1CDCM3GDXFv3GWHjS6Y6K4tnGX1kAxOo1qRHzEZyszitVJwcOj219d8w==", + "version": "56.0.26", + "resolved": "https://registry.npmjs.org/expo-build-properties/-/expo-build-properties-56.0.26.tgz", + "integrity": "sha512-0sNVshDBa4nmR+8aNiQ/57oJrlpi7yDrdlm8PMZu3g6u2xTncmVXjh2kY7n3tVbfnKPRPXmut4OgQ4/8oaZTkA==", "license": "MIT", "dependencies": { "@expo/schema-utils": "^56.0.2", @@ -5137,9 +5137,9 @@ } }, "node_modules/expo-constants": { - "version": "56.0.23", - "resolved": "https://registry.npmjs.org/expo-constants/-/expo-constants-56.0.23.tgz", - "integrity": "sha512-rlHmhwvKqljCNdqs4xdxcHzk22rbnVv05lyQqQtYXFkzreFErsQYVDvDCiw3LG6vjQRRTPTEdSgtyNgs8y9cBA==", + "version": "56.0.24", + "resolved": "https://registry.npmjs.org/expo-constants/-/expo-constants-56.0.24.tgz", + "integrity": "sha512-DxlaB0pb0Cy0QEnd7WnKVy4L5nCRQY58ePCWCTvbIVbZZ4zTt1Y1xFHeAjT+2p4PGXJ87bjWskscajyXUopP2g==", "license": "MIT", "dependencies": { "@expo/env": "~2.3.1" @@ -5164,13 +5164,13 @@ } }, "node_modules/expo-dev-client": { - "version": "56.0.24", - "resolved": "https://registry.npmjs.org/expo-dev-client/-/expo-dev-client-56.0.24.tgz", - "integrity": "sha512-Nc5EKGiq7fVP60ONHiXVh4jTNHQbajbYwGA69Ahm02hKBp27CBnGkOxHp04hkgbcN6JLD8F1Y+W8SjXb0ioUJg==", + "version": "56.0.25", + "resolved": "https://registry.npmjs.org/expo-dev-client/-/expo-dev-client-56.0.25.tgz", + "integrity": "sha512-rV2SzS3TjUU0gz2l6X97DhazUvgkpIy/RoB7bP1+ieq3ru90IPXGcLQ6O/9iNYG8tWs101jNC8U9ZHL+SLOgVw==", "license": "MIT", "dependencies": { - "expo-dev-launcher": "~56.0.25", - "expo-dev-menu": "~56.0.21", + "expo-dev-launcher": "~56.0.26", + "expo-dev-menu": "~56.0.22", "expo-dev-menu-interface": "~56.0.0", "expo-manifests": "~56.0.4", "expo-updates-interface": "~56.0.1" @@ -5180,13 +5180,13 @@ } }, "node_modules/expo-dev-launcher": { - "version": "56.0.25", - "resolved": "https://registry.npmjs.org/expo-dev-launcher/-/expo-dev-launcher-56.0.25.tgz", - "integrity": "sha512-oTFCHGpy4I1+7dd58ltNOrwKTLkYATtaRHURhZgcvJRddsFF+OMcBJbJTB6CJu5DlFyHtwVd9nNlZuo49xClWw==", + "version": "56.0.26", + "resolved": "https://registry.npmjs.org/expo-dev-launcher/-/expo-dev-launcher-56.0.26.tgz", + "integrity": "sha512-p8qM4lutkTWLFCJd/GrVXrF3au4kxUPXirHZyRqY7k/COHLw8MgsQeqcHVqGYcE/YVzpNLexq19hHumPoAO4Pw==", "license": "MIT", "dependencies": { "@expo/schema-utils": "^56.0.2", - "expo-dev-menu": "~56.0.21", + "expo-dev-menu": "~56.0.22", "expo-manifests": "~56.0.4" }, "peerDependencies": { @@ -5195,9 +5195,9 @@ } }, "node_modules/expo-dev-menu": { - "version": "56.0.21", - "resolved": "https://registry.npmjs.org/expo-dev-menu/-/expo-dev-menu-56.0.21.tgz", - "integrity": "sha512-+WSpwdT8iPAwygpuJX3qiF8q/9S9/5doRYzwBI/+eBdUY6YhLt69ajlndf6gid+IdbGDS3kgTfqB++XQ8rE6mg==", + "version": "56.0.22", + "resolved": "https://registry.npmjs.org/expo-dev-menu/-/expo-dev-menu-56.0.22.tgz", + "integrity": "sha512-xTo3WxcFll3YVnvKXo4KIOGeUwSABrnxztv1J+JzbEc77pOVQhc+ZepXz4c8fksCDBMIW0kO00O29Oe8HotfhA==", "license": "MIT", "dependencies": { "expo-dev-menu-interface": "~56.0.0" @@ -5217,9 +5217,9 @@ } }, "node_modules/expo-file-system": { - "version": "56.0.9", - "resolved": "https://registry.npmjs.org/expo-file-system/-/expo-file-system-56.0.9.tgz", - "integrity": "sha512-5M1a7+ffiJGhn/IN5kRcM8uG8V0REMciwEUPUvKykNvjFHyfKfV4USeWhuIjLvTWvDtsPiqdk56uvhFXrhR7nQ==", + "version": "56.0.10", + "resolved": "https://registry.npmjs.org/expo-file-system/-/expo-file-system-56.0.10.tgz", + "integrity": "sha512-qXX6lGnt0nIouZJMaaxqEMiWvDRTk+1aQxCzowZiZ22xD5VQIa25glX1/d+YpUhuB8Rh5auSdgFUbH+xliL+rg==", "license": "MIT", "peerDependencies": { "expo": "*", @@ -5252,9 +5252,9 @@ } }, "node_modules/expo-image": { - "version": "56.0.11", - "resolved": "https://registry.npmjs.org/expo-image/-/expo-image-56.0.11.tgz", - "integrity": "sha512-k2xwxGk14xi6zxmEGAU4rUTb1lK5qf0y0Qb8+Jaggnul0KaJJxcq9qvyDp9iyJBW35cp9isONAUnNtIiooZ/Pw==", + "version": "56.0.12", + "resolved": "https://registry.npmjs.org/expo-image/-/expo-image-56.0.12.tgz", + "integrity": "sha512-R1LDk/D/VazqcmT1OIEBV7+BDezyyJbi594ZstlklCR6aMUBNBcjQNxoEa8YaIxNdT3BsRS46/VDHpmHg4B23Q==", "license": "MIT", "dependencies": { "sf-symbols-typescript": "^2.2.0" @@ -5281,9 +5281,9 @@ } }, "node_modules/expo-image-picker": { - "version": "56.0.23", - "resolved": "https://registry.npmjs.org/expo-image-picker/-/expo-image-picker-56.0.23.tgz", - "integrity": "sha512-OvY1AhxBtukKqYtgL9nKtKRUFQfJqwsfZzg4rVDJQRD5rHJkSiW2HalCZB0ov7GFCmrBD5qe+tGgij2vweS2cA==", + "version": "56.0.24", + "resolved": "https://registry.npmjs.org/expo-image-picker/-/expo-image-picker-56.0.24.tgz", + "integrity": "sha512-5GuHVqHUhf9RM96MDZwHdsSh9F/H7DmXKnDbXlONNK8+AwTsSDwB1NSwLZgl8CxlPbZXYaGcELVJdLLscXvZng==", "license": "MIT", "dependencies": { "expo-image-loader": "~56.0.3" @@ -5310,12 +5310,12 @@ } }, "node_modules/expo-linking": { - "version": "56.0.16", - "resolved": "https://registry.npmjs.org/expo-linking/-/expo-linking-56.0.16.tgz", - "integrity": "sha512-rI/ZE22t0sgYqnE0WnDUbld/vTzOiTxEX/uKuNFGTIUZ9tmes/nvcqJufEMqRV7u6REUEIBCFxbROOBibvwG4g==", + "version": "56.0.17", + "resolved": "https://registry.npmjs.org/expo-linking/-/expo-linking-56.0.17.tgz", + "integrity": "sha512-+i2VaJlD2eamBIBetpOAkfFGYPEn01kZfRF73bbjlNiI8AkTqWWClZuJqxqHy3/R8dCCbwTTq/dJqI5dskuDvQ==", "license": "MIT", "dependencies": { - "expo-constants": "~56.0.22", + "expo-constants": "~56.0.24", "invariant": "^2.2.4" }, "peerDependencies": { @@ -5324,9 +5324,9 @@ } }, "node_modules/expo-location": { - "version": "56.0.23", - "resolved": "https://registry.npmjs.org/expo-location/-/expo-location-56.0.23.tgz", - "integrity": "sha512-MrmqbuNrefReNZ+St7YPOup9YeSSTefNmI1sGFUDQWUZ4u/xNrKzEabFMn5psIjpaWrfk/gP96gGcUqouvoR+g==", + "version": "56.0.24", + "resolved": "https://registry.npmjs.org/expo-location/-/expo-location-56.0.24.tgz", + "integrity": "sha512-ZSrqs1yoMfyovJdWQPluAxHp27FUhsGxA/PiduVk9Uz1w3ScLFJpRmOU0yUkGRCAfAnAzkaxWcemEBmOGqexmA==", "license": "MIT", "dependencies": { "@expo/image-utils": "^0.10.4" @@ -5348,9 +5348,9 @@ } }, "node_modules/expo-media-library": { - "version": "56.0.10", - "resolved": "https://registry.npmjs.org/expo-media-library/-/expo-media-library-56.0.10.tgz", - "integrity": "sha512-sTYv209n83iSt9afMH/yM4n+T80jaZcd7oxdZR6rnqvvRmPu/9sWVqsCLKCIXyisbhXSEjV+YipapErO3XSLVw==", + "version": "56.0.11", + "resolved": "https://registry.npmjs.org/expo-media-library/-/expo-media-library-56.0.11.tgz", + "integrity": "sha512-DBuEqTUwG6NNCusxvjb+gmrHI6ZqGhnFkryraViqQc8HmedtFGB45rybxjgr7zxb5lc+W8ifz3B66dasbDGXOw==", "license": "MIT", "peerDependencies": { "expo": "*", @@ -5358,9 +5358,9 @@ } }, "node_modules/expo-modules-autolinking": { - "version": "56.0.21", - "resolved": "https://registry.npmjs.org/expo-modules-autolinking/-/expo-modules-autolinking-56.0.21.tgz", - "integrity": "sha512-DG0ykIaC4cTXmEL49x7b8Pet/ZCcoNgQ00ooeroH/p8TKLMRs/KULiv9kiHWZRL3n0E0llsaHSvfKj9fAsaI/A==", + "version": "56.0.22", + "resolved": "https://registry.npmjs.org/expo-modules-autolinking/-/expo-modules-autolinking-56.0.22.tgz", + "integrity": "sha512-okvttTwsaDHzyRLp4/1qpkK2QV9gFIhp7qWZlIHzM4xCatoPSTT2dtvBD5VbXkf4whWUA7Y+DJy99hxnnUONYg==", "license": "MIT", "dependencies": { "@expo/require-utils": "^56.1.6", @@ -5373,9 +5373,9 @@ } }, "node_modules/expo-modules-core": { - "version": "56.0.23", - "resolved": "https://registry.npmjs.org/expo-modules-core/-/expo-modules-core-56.0.23.tgz", - "integrity": "sha512-5XICEk65GXP37g87GSxivHydyL8mizvVMUfF3YFtt6zQtniuPqkWfEFp35LqnaDCxC0OLAAtMPW9P/dRWBe78A==", + "version": "56.0.24", + "resolved": "https://registry.npmjs.org/expo-modules-core/-/expo-modules-core-56.0.24.tgz", + "integrity": "sha512-cwBCdPS0lRM2jEmSUhF9vV//HnjJ/Ysb8EwkwxQW7ltSU/YvPNulfRhF4UsdXxw/mUfHvrH7AF+i6oxu3pHtMw==", "license": "MIT", "dependencies": { "@expo/expo-modules-macros-plugin": "0.2.2", @@ -5403,15 +5403,15 @@ } }, "node_modules/expo-router": { - "version": "56.2.18", - "resolved": "https://registry.npmjs.org/expo-router/-/expo-router-56.2.18.tgz", - "integrity": "sha512-Ql19oNtVIz0+0JY74IVl+wTP3vCUNNSNeK9Zv/jQ7dbkB20oTh3TXXDsoDohmxTXOFXf+yL6F2qTZAHPEu3J+w==", + "version": "56.2.19", + "resolved": "https://registry.npmjs.org/expo-router/-/expo-router-56.2.19.tgz", + "integrity": "sha512-SnhQjmQv3pauoVYUaCVI9AikKNiO7FXBgrQ0oVw/fn2ivP79GKElP7opEOlNOBE9gsGrHrsA3Co3xFXIf19ujA==", "license": "MIT", "dependencies": { "@expo/log-box": "^56.0.14", - "@expo/metro-runtime": "^56.0.19", + "@expo/metro-runtime": "^56.0.20", "@expo/schema-utils": "^56.0.2", - "@expo/ui": "^56.0.25", + "@expo/ui": "^56.0.26", "@radix-ui/react-slot": "^1.2.0", "@radix-ui/react-tabs": "^1.1.12", "@react-native-masked-view/masked-view": "^0.3.2", @@ -5422,7 +5422,7 @@ "debug": "^4.3.4", "escape-string-regexp": "^4.0.0", "expo-glass-effect": "^56.0.4", - "expo-server": "^56.0.5", + "expo-server": "^56.0.6", "expo-symbols": "^56.0.7", "fast-deep-equal": "^3.1.3", "invariant": "^2.2.4", @@ -5440,11 +5440,11 @@ }, "peerDependencies": { "@expo/log-box": "^56.0.14", - "@expo/metro-runtime": "^56.0.19", + "@expo/metro-runtime": "^56.0.20", "@testing-library/react-native": ">= 13.2.0", "expo": "*", - "expo-constants": "^56.0.23", - "expo-linking": "^56.0.16", + "expo-constants": "^56.0.24", + "expo-linking": "^56.0.17", "react": "*", "react-dom": "*", "react-native": "*", @@ -5522,21 +5522,21 @@ } }, "node_modules/expo-server": { - "version": "56.0.5", - "resolved": "https://registry.npmjs.org/expo-server/-/expo-server-56.0.5.tgz", - "integrity": "sha512-SmM2p2g3Jrktpiazcst+OxhjSzOHXKAY4BPURHYHXvApzzoybMmrNF4IEZ8DKZ145BhSe4ydAmlEFCRTsdtgUQ==", + "version": "56.0.6", + "resolved": "https://registry.npmjs.org/expo-server/-/expo-server-56.0.6.tgz", + "integrity": "sha512-3749PixW3Fs42lFSvi8wYBbMNexb/lVtVyN50BhT0hzX37bT9HrJD4siokV5InThAkpFwdfxGQdWNuAECg9ZCg==", "license": "MIT", "engines": { "node": ">=20.16.0" } }, "node_modules/expo-sharing": { - "version": "56.0.24", - "resolved": "https://registry.npmjs.org/expo-sharing/-/expo-sharing-56.0.24.tgz", - "integrity": "sha512-+pk1u/TdrGKGdLDtRyMiZG5DGrNhbjjHRLA5vU6gj+bQmLtpBpfrjenHLWDLBEvhFHF02FV37lqBRVpF/hFIiw==", + "version": "56.0.25", + "resolved": "https://registry.npmjs.org/expo-sharing/-/expo-sharing-56.0.25.tgz", + "integrity": "sha512-X3QviZbMuWrizYmsGojmbahoKBappNe8AvGl+g+Heh1Ebgr3thHotJbBOkgTK5scBszA3qc83MZTtHJ3gAtRaQ==", "license": "MIT", "dependencies": { - "@expo/config-plugins": "^56.0.14", + "@expo/config-plugins": "^56.0.15", "@expo/config-types": "^56.0.7", "@expo/plist": "^0.7.0" }, @@ -5583,14 +5583,14 @@ } }, "node_modules/expo/node_modules/@expo/cli": { - "version": "56.1.23", - "resolved": "https://registry.npmjs.org/@expo/cli/-/cli-56.1.23.tgz", - "integrity": "sha512-a6FU2K0rIGaXwRsX/wEllXIryIkM0TynL146UDaQEQHbGnMya7RLchSa1WKFrN0V0NTCrEEo0YBUDA+vlYle1Q==", + "version": "56.1.24", + "resolved": "https://registry.npmjs.org/@expo/cli/-/cli-56.1.24.tgz", + "integrity": "sha512-zWx4kHuZQrq1yz/uF6ADeXVvzG8N2gUj/Zf7eF6L8eALvEeX0uCBDmpdo5Yj/fY4sLo1N2VEH62StDRqnnSOxg==", "license": "MIT", "dependencies": { "@expo/code-signing-certificates": "^0.0.6", "@expo/config": "~56.0.13", - "@expo/config-plugins": "~56.0.14", + "@expo/config-plugins": "~56.0.15", "@expo/devcert": "^1.2.1", "@expo/env": "~2.3.1", "@expo/image-utils": "^0.10.4", @@ -5603,9 +5603,9 @@ "@expo/osascript": "^2.6.0", "@expo/package-manager": "^1.12.1", "@expo/plist": "^0.7.0", - "@expo/prebuild-config": "^56.0.21", + "@expo/prebuild-config": "^56.0.22", "@expo/require-utils": "^56.1.6", - "@expo/router-server": "^56.0.17", + "@expo/router-server": "^56.0.18", "@expo/schema-utils": "^56.0.2", "@expo/spawn-async": "^1.8.0", "@expo/ws-tunnel": "^2.0.0", @@ -5622,12 +5622,12 @@ "connect": "^3.7.0", "debug": "^4.3.4", "dnssd-advertise": "^1.1.6", - "expo-server": "^56.0.5", + "expo-server": "^56.0.6", "fetch-nodeshim": "^0.4.10", "getenv": "^2.0.0", "glob": "^13.0.0", "lan-network": "^0.2.1", - "multitars": "^1.0.0", + "multitars": "^1.0.2", "node-forge": "^1.3.3", "npm-package-arg": "^11.0.0", "ora": "^3.4.0", @@ -5636,6 +5636,7 @@ "progress": "^2.0.3", "prompts": "^2.3.2", "resolve-from": "^5.0.0", + "sandbox-cli-detector": "^0.2.0", "semver": "^7.6.0", "send": "^0.19.0", "slugify": "^1.3.4", @@ -5665,20 +5666,20 @@ } }, "node_modules/expo/node_modules/@expo/cli/node_modules/@expo/router-server": { - "version": "56.0.17", - "resolved": "https://registry.npmjs.org/@expo/router-server/-/router-server-56.0.17.tgz", - "integrity": "sha512-ogGX5cxqAXIzMmstjwZ03QAjCU8cFOU5yooDSbqPDVxscbnzsxV8oquHSMUsqiEy3vlyZp5S5HVPnCsrjoCDqw==", + "version": "56.0.18", + "resolved": "https://registry.npmjs.org/@expo/router-server/-/router-server-56.0.18.tgz", + "integrity": "sha512-3Ko9M9yIu9pcWcNik55JT3DYrODumIFkrRN6xbL+HtUFgiQp6N02gt6LngPRu0r7YKWYvHU12rsM3MDtucs63Q==", "license": "MIT", "dependencies": { "debug": "^4.3.4" }, "peerDependencies": { - "@expo/metro-runtime": "^56.0.18", + "@expo/metro-runtime": "^56.0.20", "expo": "*", - "expo-constants": "^56.0.22", + "expo-constants": "^56.0.24", "expo-font": "^56.0.7", "expo-router": "*", - "expo-server": "^56.0.5", + "expo-server": "^56.0.6", "react": "*", "react-dom": "*", "react-server-dom-webpack": "~19.0.1 || ~19.1.2 || ~19.2.1" @@ -7509,9 +7510,9 @@ "license": "MIT" }, "node_modules/multitars": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/multitars/-/multitars-1.0.1.tgz", - "integrity": "sha512-Do9rHaSDfQ2Fk5Dpg2RjQ4M48Ol7rSq1gvKn/dXJYnhKEm8RRjjUvyiGDDEhJqb0hJPGjRaTB1kx6beqKO/i6Q==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/multitars/-/multitars-1.0.2.tgz", + "integrity": "sha512-6GwVw5eLi9sThdtlS4PKwC7yRLaf45pYhIEzKBHdKxi+YOXGKFX8acIniH+Uh/+k9mS2lQOupTccjoe5r0/1IQ==", "license": "MIT" }, "node_modules/mz": { @@ -7526,9 +7527,9 @@ } }, "node_modules/nanoid": { - "version": "3.3.16", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", - "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", + "version": "3.3.18", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", + "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", "funding": [ { "type": "github", @@ -9347,6 +9348,18 @@ ], "license": "MIT" }, + "node_modules/sandbox-cli-detector": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/sandbox-cli-detector/-/sandbox-cli-detector-0.2.0.tgz", + "integrity": "sha512-4lyHX0ZU0AZKwjgZ1InxZAa3PNpyEb8rOQ+Zss1ReYmhNzW0Q+h1zE5nvniXN0HaAWZaZE1zgVNEirb0R7LmNg==", + "license": "MIT", + "bin": { + "sandbox-cli-detector": "dist/cli.js" + }, + "engines": { + "node": ">=18.18" + } + }, "node_modules/sax": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.0.tgz", diff --git a/package.json b/package.json index 543d86b..2fb5cf2 100644 --- a/package.json +++ b/package.json @@ -6,22 +6,22 @@ "@expo/vector-icons": "^15.1.1", "@react-native-async-storage/async-storage": "2.2.0", "@tanstack/react-query": "^5.100.14", - "expo": "~56.0.19", - "expo-build-properties": "~56.0.25", + "expo": "~56.0.20", + "expo-build-properties": "~56.0.26", "expo-camera": "~56.0.8", "expo-constants": "~56.0.21", - "expo-dev-client": "~56.0.24", + "expo-dev-client": "~56.0.25", "expo-file-system": "~56.0.7", "expo-font": "~56.0.5", - "expo-image": "~56.0.11", - "expo-image-picker": "~56.0.23", + "expo-image": "~56.0.12", + "expo-image-picker": "~56.0.24", "expo-linear-gradient": "~56.0.4", - "expo-linking": "~56.0.16", - "expo-location": "~56.0.23", - "expo-media-library": "~56.0.10", - "expo-router": "~56.2.18", + "expo-linking": "~56.0.17", + "expo-location": "~56.0.24", + "expo-media-library": "~56.0.11", + "expo-router": "~56.2.19", "expo-secure-store": "~56.0.4", - "expo-sharing": "~56.0.24", + "expo-sharing": "~56.0.25", "expo-status-bar": "~56.0.4", "nativewind": "^4.2.4", "react": "19.2.3", @@ -73,6 +73,7 @@ }, "private": true, "overrides": { + "nanoid": "^3.3.18", "uuid": "^11.1.1" } } From 6fdd37f56ea2f3bde7c3b48586ff5d7c80f5d8b5 Mon Sep 17 00:00:00 2001 From: manNomi Date: Fri, 21 Aug 2026 01:00:08 +0900 Subject: [PATCH 4/5] =?UTF-8?q?fix:=20=EA=B3=B5=EA=B0=9C=20=EC=A7=80?= =?UTF-8?q?=EC=9B=90=20=EB=A9=94=EC=9D=BC=EC=9D=84=20=EC=9A=B4=EC=98=81=20?= =?UTF-8?q?=EC=A3=BC=EC=86=8C=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app.config.js | 2 +- eas.json | 2 +- src/constants/legal.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app.config.js b/app.config.js index bfc65f7..f82b73e 100644 --- a/app.config.js +++ b/app.config.js @@ -185,7 +185,7 @@ const baseConfig = { privacyPolicyUrl: process.env.EXPO_PUBLIC_SOUNDLOG_PRIVACY_URL ?? 'https://api.soundlog.p-e.kr/legal/privacy', - supportEmail: process.env.EXPO_PUBLIC_SOUNDLOG_SUPPORT_EMAIL ?? 'support@soundlog.shop', + supportEmail: process.env.EXPO_PUBLIC_SOUNDLOG_SUPPORT_EMAIL ?? 'hcy032020@gmail.com', termsUrl: process.env.EXPO_PUBLIC_SOUNDLOG_TERMS_URL ?? 'https://api.soundlog.p-e.kr/legal/terms', diff --git a/eas.json b/eas.json index 52c80b0..88e2c84 100644 --- a/eas.json +++ b/eas.json @@ -27,7 +27,7 @@ "EXPO_PUBLIC_SOUNDLOG_API_BASE_URL": "https://api.soundlog.p-e.kr", "EXPO_PUBLIC_SOUNDLOG_API_SOURCE": "server", "EXPO_PUBLIC_SOUNDLOG_PRIVACY_URL": "https://api.soundlog.p-e.kr/legal/privacy", - "EXPO_PUBLIC_SOUNDLOG_SUPPORT_EMAIL": "support@soundlog.shop", + "EXPO_PUBLIC_SOUNDLOG_SUPPORT_EMAIL": "hcy032020@gmail.com", "EXPO_PUBLIC_SOUNDLOG_TERMS_URL": "https://api.soundlog.p-e.kr/legal/terms", "EXPO_PUBLIC_SOUNDLOG_UPLOAD_ORIGIN": "https://api.soundlog.p-e.kr" } diff --git a/src/constants/legal.ts b/src/constants/legal.ts index 805813f..72b94a4 100644 --- a/src/constants/legal.ts +++ b/src/constants/legal.ts @@ -1,5 +1,5 @@ export const SOUNDLOG_SUPPORT_EMAIL = - process.env.EXPO_PUBLIC_SOUNDLOG_SUPPORT_EMAIL ?? 'support@soundlog.shop'; + process.env.EXPO_PUBLIC_SOUNDLOG_SUPPORT_EMAIL ?? 'hcy032020@gmail.com'; export const SOUNDLOG_PRIVACY_URL = process.env.EXPO_PUBLIC_SOUNDLOG_PRIVACY_URL ?? From 5006fed71186e1dfdda3c6eac081bdeba64fa065 Mon Sep 17 00:00:00 2001 From: manNomi Date: Fri, 21 Aug 2026 12:53:59 +0900 Subject: [PATCH 5/5] =?UTF-8?q?feat:=20=EA=B4=80=EA=B4=91=EC=A7=80=20?= =?UTF-8?q?=EB=A7=88=EC=BB=A4=20=EC=A0=95=EB=B3=B4=20=ED=8C=A8=EB=84=90=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/(tabs)/index.tsx | 21 +++- .../travel/recap-map/RecapMapSection.tsx | 54 +++++++-- .../recap-map/SelectedTourPlacePanel.tsx | 109 ++++++++++++++++++ 3 files changed, 170 insertions(+), 14 deletions(-) create mode 100644 src/components/travel/recap-map/SelectedTourPlacePanel.tsx diff --git a/app/(tabs)/index.tsx b/app/(tabs)/index.tsx index 3fcdafc..d6b7f88 100644 --- a/app/(tabs)/index.tsx +++ b/app/(tabs)/index.tsx @@ -1,5 +1,5 @@ import { Redirect, router } from "expo-router"; -import { useEffect, useState } from "react"; +import { useEffect, useMemo, useState } from "react"; import { View } from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; @@ -77,14 +77,24 @@ export default function MapHomeScreen() { { enabled: status === "authenticated" && session.status === "active" }, ); const nearestTourPlace = currentLocation - ? nearbyPlacesQuery.data?.find( - (place) => place.source === "tour-api" && Boolean(place.location), - ) + ? nearbyPlacesQuery.data?.find((place) => Boolean(place.location)) : undefined; const cachedTourPlace = - currentPlace?.source === "tour-api" && currentPlace.location + currentPlace?.location && + (currentPlace.source === "tour-api" || currentPlace.source === "seed") ? currentPlace : undefined; + const nearbyTourPlaces = useMemo(() => { + const places = (nearbyPlacesQuery.data ?? []).filter((place) => + Boolean(place.location), + ); + + if (places.length > 0) { + return places; + } + + return cachedTourPlace ? [cachedTourPlace] : []; + }, [cachedTourPlace, nearbyPlacesQuery.data]); const activeCurrentPlace = currentLocation ? nearestTourPlace : cachedTourPlace; @@ -366,6 +376,7 @@ export default function MapHomeScreen() { overlayBottomInset={mapOverlayBottomInset} overlayTopInset={mapOverlayTopInset} sessionStatus={session.status} + tourPlaces={nearbyTourPlaces} tourPlaceStatus={tourPlaceStatus} variant="page" /> diff --git a/src/components/travel/recap-map/RecapMapSection.tsx b/src/components/travel/recap-map/RecapMapSection.tsx index 366bd38..ff32c67 100644 --- a/src/components/travel/recap-map/RecapMapSection.tsx +++ b/src/components/travel/recap-map/RecapMapSection.tsx @@ -17,6 +17,7 @@ import type { SoundMapViewportSize, } from '../live-sound-map/types'; import { SelectedRecapPinPanel } from './SelectedRecapPinPanel'; +import { SelectedTourPlacePanel } from './SelectedTourPlacePanel'; type RecapMapFilter = 'mine' | 'place' | 'public'; type RecapMapPinGroup = { @@ -36,6 +37,7 @@ type RecapMapSectionProps = { overlayBottomInset?: number; overlayTopInset?: number; sessionStatus: 'active' | 'ended' | 'idle'; + tourPlaces?: PlaceContext[]; tourPlaceStatus?: TourPlaceStatus; variant?: 'page' | 'section'; }; @@ -207,6 +209,7 @@ export function RecapMapSection({ overlayBottomInset = 112, overlayTopInset = 12, sessionStatus, + tourPlaces = [], tourPlaceStatus = currentPlace ? 'ready' : 'unavailable', variant = 'section', }: RecapMapSectionProps) { @@ -232,9 +235,23 @@ export function RecapMapSection({ const placeName = getTourPlaceLabel(currentPlace, tourPlaceStatus); const scope = getScope(filter); const visibleMarkers = serverMarkers; - const placePin = useMemo( - () => (currentPlace ? createPlacePin(currentPlace) : undefined), - [currentPlace], + const visibleTourPlaces = useMemo(() => { + const placesById = new Map(); + + [currentPlace, ...tourPlaces].forEach((place) => { + if (place?.location) { + placesById.set(place.id, place); + } + }); + + return Array.from(placesById.values()); + }, [currentPlace, tourPlaces]); + const placePins = useMemo( + () => + visibleTourPlaces + .map(createPlacePin) + .filter((pin): pin is SoundMapPin => Boolean(pin)), + [visibleTourPlaces], ); const clusteringViewport = useMemo( () => @@ -249,19 +266,33 @@ export function RecapMapSection({ ); const mapPinGroups = useMemo(() => { if (filter === 'place') { - return placePin ? [{ markers: [], pin: placePin }] : []; + return placePins.map((pin) => ({ markers: [], pin })); } return toRecapMapPinGroups(visibleMarkers, filter, clusteringViewport); - }, [clusteringViewport, filter, placePin, visibleMarkers]); + }, [clusteringViewport, filter, placePins, visibleMarkers]); const mapPins = useMemo(() => mapPinGroups.map((group) => group.pin), [mapPinGroups]); const selectedPinGroup = useMemo( () => mapPinGroups.find((group) => group.pin.id === selectedPinId), [mapPinGroups, selectedPinId], ); + const selectedTourPlace = useMemo( + () => + visibleTourPlaces.find( + (place) => `tour-place-${place.id}` === selectedPinId, + ), + [selectedPinId, visibleTourPlaces], + ); const viewportKey = useMemo(() => { if (filter === 'place') { - return `${filter}:${placePin?.id ?? 'empty'}`; + const placeKey = placePins + .map( + (pin) => + `${pin.id}:${pin.location.lat.toFixed(6)}:${pin.location.lng.toFixed(6)}`, + ) + .join('|'); + + return `${filter}:${placeKey || 'empty'}`; } const markerKey = visibleMarkers @@ -273,7 +304,7 @@ export function RecapMapSection({ .join('|'); return `${filter}:${markerKey}`; - }, [filter, placePin?.id, visibleMarkers]); + }, [filter, placePins, visibleMarkers]); const showTravelCta = filter === 'place'; const mapTitle = filter === 'mine' @@ -436,7 +467,7 @@ export function RecapMapSection({ legendItems={getMapLegendItems(filter)} onRegionChangeComplete={handleRegionChangeComplete} onViewportLayoutChange={handleViewportLayoutChange} - onPinPress={filter === 'place' ? undefined : (pin) => setSelectedPinId(pin.id)} + onPinPress={(pin) => setSelectedPinId(pin.id)} pins={mapPins} ref={mapViewRef} selectedPinId={selectedPinId} @@ -490,7 +521,12 @@ export function RecapMapSection({ - {showTravelCta ? ( + {selectedTourPlace ? ( + setSelectedPinId(undefined)} + place={selectedTourPlace} + /> + ) : showTravelCta ? ( void; + place: PlaceContext; +}; + +function getPlaceMeta(place: PlaceContext) { + const distance = place.distanceMeters; + const distanceLabel = + distance === undefined + ? undefined + : distance >= 1000 + ? `${(distance / 1000).toFixed(1)}km` + : `${Math.round(distance).toLocaleString('ko-KR')}m`; + + return [ + place.category ?? place.contentType, + distanceLabel, + ] + .filter(Boolean) + .join(' · '); +} + +export function SelectedTourPlacePanel({ + onClose, + place, +}: SelectedTourPlacePanelProps) { + const placeMeta = getPlaceMeta(place); + + return ( + + + + {place.imageUrl ? ( + + ) : ( + + + + )} + + + + + + + 선택한 관광지 + + + {place.title} + + + + + + + + {placeMeta ? ( + + {placeMeta} + + ) : null} + + + + {place.address ? ( + + + + {place.address} + + + ) : null} + + {place.overview ? ( + + {place.overview} + + ) : null} + + + {place.attribution ?? + (place.source === 'tour-api' ? '한국관광공사 관광정보' : '관광지 정보')} + + + ); +}