Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
a049b93
feat : TJ 공식 차트 연동 기능 추가 (#295)
GulSam00 Aug 5, 2026
471a452
chore : 불일치 로그 업데이트
GulSam00 Aug 5, 2026
feeee00
feat : TJ 차트 월 이동 화살표 및 장르 뱃지 필터 적용 (#295)
GulSam00 Aug 10, 2026
a8c4fd1
fix : TJ 차트 API 요청 timeout 및 월 파라미터 처리 보완 (#295)
GulSam00 Aug 10, 2026
8a22dd8
doc : crawling CLAUDE.md TJ 차트 파이프라인 반영 (#295)
GulSam00 Aug 10, 2026
950e148
feat : 인기차트 제목 분리 및 장르 뱃지 이모지·휠 가로 스크롤 적용 (#295)
GulSam00 Aug 11, 2026
a29e060
feat : TJ 반주 버전 뱃지 수집 파이프라인 추가 (#295)
GulSam00 Aug 11, 2026
29574a5
chore : TJ 백필 구간 갱신 및 수집 로그 추가 (#295)
GulSam00 Aug 11, 2026
165e644
doc : crawling CLAUDE.md 뱃지 파이프라인 반영 (#295)
GulSam00 Aug 11, 2026
29ada6a
feat : 차트 미매칭 곡 자동 보충 및 API 타임아웃·재시도 보완 (#295)
GulSam00 Aug 11, 2026
f233e83
chore : 해소된 TJ 차트 미매칭 로그 정리 (#295)
GulSam00 Aug 11, 2026
5152aef
chore : TJ 차트 워크플로우에 미매칭 로그 아티팩트 업로드 추가 (#295)
GulSam00 Aug 11, 2026
328cb03
feat : 인기차트에 TJ 반주 버전 뱃지 노출 (#295)
GulSam00 Aug 11, 2026
623314f
feat : 검색 결과에도 반주 버전 뱃지 표시 및 UI 통일 (#295)
GulSam00 Aug 11, 2026
706021c
feat : TJ에서 사라진 번호의 곡 정리 스크립트 추가 (#295)
GulSam00 Aug 11, 2026
d5c09a3
fix : 60 뱃지 라벨에 TJ 명시 (#295)
GulSam00 Aug 11, 2026
285f61d
fix : 뱃지 묶음 앞에 TJ 표식을 붙여 출처 명시 (#295)
GulSam00 Aug 11, 2026
89a5563
fix : TJ 표식을 뱃지 테두리 안으로 이동 (#295)
GulSam00 Aug 11, 2026
0f65d84
fix : 신곡이 죽은 곡으로 오인돼 삭제되던 문제 (#295)
GulSam00 Aug 11, 2026
9555959
feat : 장르 필터 스크롤바 노출 및 이모지 교체 (#295)
GulSam00 Aug 11, 2026
66a46fc
feat : 가이드 예시 카드 가상화, 검색 추천 제거, 곡 카드 디자인 통일 (#295)
GulSam00 Aug 12, 2026
5ad9e3a
fix : 60 뱃지에만 TJ 표식 유지, MR·LIVE는 제거 (#295)
GulSam00 Aug 12, 2026
88678c5
fix : 번호 검색에서 언어 필터 숨김 및 값 초기화 (#295)
GulSam00 Aug 12, 2026
d95acce
fix : 국가 뱃지 줄바꿈 대신 가로 스크롤 (#295)
GulSam00 Aug 12, 2026
d3c104b
Merge pull request #296 from GulSam00/feat/295-tjChartApi
GulSam00 Aug 12, 2026
9429e97
chore : TWA targetSdkVersion 36 대응 (#297)
GulSam00 Aug 13, 2026
98febea
doc : TWA 빌드 절차 및 트러블슈팅 보강 (#297)
GulSam00 Aug 13, 2026
8ac0985
Merge pull request #298 from GulSam00/chore/297-twaTargetSdk36
GulSam00 Aug 13, 2026
9906000
feat : 인기차트 배경 박스 제거 및 년-월 선택기 확장 (#299)
GulSam00 Aug 13, 2026
a7697ba
fix : 최신곡 페이지 제목 좌측 상단 배치 (#299)
GulSam00 Aug 13, 2026
f9f8ac3
chore : sitemap 재생성 (#299)
GulSam00 Aug 13, 2026
c9a52fb
Merge pull request #300 from GulSam00/feat/299-popularChartUi
GulSam00 Aug 13, 2026
ce36265
chore : 버전 2.11.0 및 패치노트 추가 (#301)
GulSam00 Aug 13, 2026
66322aa
Merge pull request #302 from GulSam00/chore/301-version2110
GulSam00 Aug 13, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions .github/workflows/crawl_recent_tj.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,18 @@ jobs:
- name: run crawl script - crawlRecentTJ.ts
working-directory: packages/crawling
run: pnpm run recent-tj

# recent-tj가 넣는 신곡은 badges가 null이다. 그대로 두면 뱃지가 표시되지 않고,
# badges is null을 판정 기준으로 쓰는 정리 스크립트가 신곡을 죽은 곡으로 오인한다.
- name: run crawl script - crawlTjBadges.ts
working-directory: packages/crawling
run: pnpm run tj-badges

- name: Upload badge log
if: always()
uses: actions/upload-artifact@v4
with:
name: tj-badges-log-${{ github.run_id }}
path: packages/crawling/src/assets/tjBadgeErrors.txt
if-no-files-found: ignore
retention-days: 30
13 changes: 13 additions & 0 deletions .github/workflows/crawl_tj_all_number.yml
Original file line number Diff line number Diff line change
Expand Up @@ -59,3 +59,16 @@ jobs:
START_NUMBER: ${{ matrix.range.start }}
END_NUMBER: ${{ matrix.range.end }}
run: pnpm run tj-all-number

# 변경 내역은 러너 파일시스템에만 쌓이므로 잡이 끝나면 사라진다.
# 실패해도(if: always()) 그때까지의 기록을 받을 수 있게 아티팩트로 올린다.
- name: Upload change log
if: always()
uses: actions/upload-artifact@v4
with:
name: tj-all-number-log-${{ matrix.range.start }}-${{ matrix.range.end }}
path: |
packages/crawling/src/assets/tjAllNumberLog.txt
packages/crawling/src/assets/tjAllNumberProgress.json
if-no-files-found: ignore
retention-days: 30
49 changes: 49 additions & 0 deletions .github/workflows/crawl_tj_chart.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
name: Crawl TJ Chart

on:
schedule:
- cron: "0 1 1 * *" # 매달 1일 KST 오전 10:00 실행 (UTC+9 → UTC 01:00)
workflow_dispatch:

jobs:
run-npm-task:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4

- name: Use Node.js 20
uses: actions/setup-node@v4
with:
node-version: "20"

- name: Install pnpm
uses: pnpm/action-setup@v2
with:
version: 9
run_install: false

- name: Install dependencies
working-directory: packages/crawling
run: pnpm install

- name: Create .env file
working-directory: packages/crawling
run: |
echo "SUPABASE_URL=${{ secrets.SUPABASE_URL }}" >> .env
echo "SUPABASE_KEY=${{ secrets.SUPABASE_KEY }}" >> .env

- name: run crawl script - crawlTjChart.ts
working-directory: packages/crawling
run: pnpm run tj-chart

# 미매칭·경고 로그는 러너 파일시스템에만 쌓여 잡이 끝나면 사라진다.
# 실패해도(if: always()) 그때까지의 기록을 받을 수 있게 아티팩트로 올린다.
- name: Upload unmatched log
if: always()
uses: actions/upload-artifact@v4
with:
name: tj-chart-unmatched-${{ github.run_id }}
path: packages/crawling/src/assets/tjChartUnmatched.txt
if-no-files-found: ignore
retention-days: 30
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ No test suite is configured.
```
apps/
web/ — Next.js 15 web app (primary app, see apps/web/CLAUDE.md)
twa/ — Bubblewrap TWA build workspace for Play Store (see apps/twa/CLAUDE.md)
mobile/ — [DEPRECATED — frozen, see apps/mobile/README.md] Expo wrapper app, replaced by TWA approach. Excluded from pnpm workspace.
packages/
open-api/ — Wrapper around the external karaoke open API (@repo/open-api)
Expand Down
94 changes: 89 additions & 5 deletions apps/twa/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,16 +45,19 @@ pnpm install
pnpm --filter twa run init

# Android 프로젝트 생성 + 서명된 .aab/.apk 빌드
pnpm --filter twa run build
pnpm --filter twa run build:android

# webmanifest 변경 사항을 twa-manifest.json에 반영
pnpm --filter twa run update

# app/build.gradle의 targetSdk / 버전이 올바른지만 확인
pnpm --filter twa run check-sdk
```

또는 `apps/twa/`에서 직접:

```bash
pnpm build # bubblewrap build
pnpm run build:android # bubblewrap build + targetSdk 검증
```

## Build Workflow (재현 가능한 절차)
Expand Down Expand Up @@ -147,7 +150,45 @@ curl -sI https://www.singcode.kr/.well-known/assetlinks.json # 200
### 6. `bubblewrap build`

```bash
pnpm --filter twa run build
pnpm --filter twa run build:android
```

빌드가 끝나면 `scripts/verify-target-sdk.mjs`가 산출물의 targetSdk와 versionCode를 검사한다.
검사에 실패하면 빌드 명령이 0이 아닌 코드로 종료되므로, 잘못된 산출물을 Play Console에 올릴 일이 없다.

> 💡 **build 도중 "regenerate Android project" 프롬프트가 뜨면 `Yes`를 선택한다.**
> `twa-manifest.json`을 수정하면 checksum이 달라져 이 프롬프트가 뜬다.
> Bubblewrap 1.25.0 템플릿은 `targetSdkVersion 36`이므로 재생성해도 안전하고,
> 오히려 `versionCode`/`versionName`이 `twa-manifest.json` 기준으로 갱신되어 반영된다.

버전은 `bubblewrap update`가 실행될 때 **자동으로 1씩 올라간다** (`Upgraded app version to versionName: N`).
수동으로 지정하려면 `twa-manifest.json`의 `appVersionCode`/`appVersionName`/`appVersion`을 수정한다.
`app/build.gradle`은 재생성 시 그 값으로 채워지므로 직접 편집하지 않는다.

<a id="target-sdk"></a>

#### targetSdkVersion 36 (Google Play 요구사항)

Google Play는 **2026-08-31**부터 API 36(Android 16) 미만을 타겟하는 앱의 업데이트를 거부한다.

`targetSdkVersion`은 Bubblewrap 템플릿(`template_project/app/build.gradle`)에
**리터럴로 하드코딩**되어 있다. `minSdkVersion`과 달리 `twa-manifest.json`으로 제어할 수 없고,
`app/build.gradle`은 `.gitignore` 대상이라 커밋으로 고정할 수도 없다.
따라서 **어떤 Bubblewrap 버전을 쓰느냐가 targetSdk를 결정한다.**

| 버전 | 템플릿의 targetSdkVersion |
| -------- | ------------------------- |
| ~1.24.1 | 35 ❌ |
| 1.25.0~ | 36 ✅ |

`@bubblewrap/cli`는 **1.25.0 이상**을 유지한다. 다운그레이드하면 targetSdk가 35로 돌아간다.
이미 생성된 `app/build.gradle`은 CLI를 올려도 자동으로 갱신되지 않으므로,
버전을 올린 뒤에는 빌드 시 재생성 프롬프트에 `Yes`로 답해 새 템플릿을 반영한다.

빌드와 무관하게 현재 상태만 확인하려면:

```bash
pnpm --filter twa run check-sdk
```

산출물:
Expand Down Expand Up @@ -191,6 +232,31 @@ curl -s https://www.singcode.kr/.well-known/assetlinks.json | jq .

배포 직후 디바이스에서 즉시 반영되지 않을 수 있다 → 앱 데이터 삭제 후 재설치.

### `EBUSY: resource busy or locked` — Gradle 데몬이 파일을 잡고 있다

```
cli ERROR EBUSY: resource busy or locked, unlink 'apps/twa/app/build/intermediates/.../classes.dex'
```

Gradle 데몬이 살아 있으면 Bubblewrap이 프로젝트를 재생성하지 못한다.
이 상태로 실패하면 `gradlew`, `settings.gradle` 등이 **부분적으로 삭제된 채 남을 수 있다.**

```bash
# 1. 데몬 종료 (gradlew가 남아 있으면 이 방법이 우선)
cd apps/twa && ./gradlew --stop

# 2. gradlew도 지워졌다면 Gradle 데몬 프로세스를 직접 종료
powershell -NoProfile -Command "Get-CimInstance Win32_Process -Filter \"name='java.exe'\" | Where-Object { \$_.CommandLine -like '*GradleDaemon*' } | Select-Object ProcessId, CommandLine"
taskkill /PID <해당 PID> /F

# 3. 빌드 캐시 제거 후 재생성
rm -rf apps/twa/app/build
pnpm --filter twa run update
```

> IDE(Android Studio, VS Code Java 확장 등)가 `apps/twa`를 Gradle 프로젝트로 열어두면
> 자체 데몬을 유지한다. 반복해서 잠긴다면 해당 프로젝트를 IDE에서 닫는다.

### `bubblewrap build` 실패: SDK / build-tools 미발견

`ANDROID_HOME`과 `JAVA_HOME` 환경 변수가 설정되어 있는지, `cmdline-tools/latest/bin`이 PATH에 있는지 확인한다.
Expand All @@ -203,14 +269,32 @@ PWA의 webmanifest를 수정했다면:
pnpm --filter twa run update
```

또는 `twa-manifest.json`을 직접 편집한 후 `pnpm --filter twa run build`로 재빌드.
또는 `twa-manifest.json`을 직접 편집한 후 `pnpm --filter twa run build:android`로 재빌드.
이때 checksum이 달라져 재생성 프롬프트가 뜨는데, **`Yes`를 선택**하면 변경된 버전이 반영된다.

### 빌드 후 targetSdk 검사에서 실패한다

```
[target-sdk] ❌ app/build.gradle의 targetSdkVersion이 35입니다 (필요: 36).
```

`app/build.gradle`이 1.24.x 이하 템플릿으로 생성된 상태다.
`pnpm --filter twa exec bubblewrap --version`으로 1.25.0 이상인지 확인한 뒤,
빌드 시 재생성 프롬프트에 `Yes`로 답해 새 템플릿을 반영한다.

```
[target-sdk] ❌ versionCode 불일치 — app/build.gradle(10) vs twa-manifest.json(11).
```

`twa-manifest.json`의 버전 변경이 아직 반영되지 않았다. 재생성 프롬프트에 `Yes`로 답해 다시 빌드한다.

## Files in this workspace

| 파일 | 설명 | 커밋 여부 |
| --------------------- | ---------------------------------------------------------- | --------- |
| `package.json` | 워크스페이스 메타 + bubblewrap 스크립트 | ✅ 커밋 |
| `twa-manifest.json` | Bubblewrap 빌드 설정 (Application ID, 색상, 아이콘 URL 등) | ✅ 커밋 |
| `twa-manifest.json` | Bubblewrap 빌드 설정 (Application ID, 색상, 아이콘 URL 등) — **버전의 single source of truth** | ✅ 커밋 |
| `scripts/verify-target-sdk.mjs` | 빌드 산출물의 targetSdk 36 / versionCode 검사 ([설명](#target-sdk)) | ✅ 커밋 |
| `.gitignore` | 키스토어, .aab/.apk, `android/`, `output/` 차단 | ✅ 커밋 |
| `CLAUDE.md` | 본 문서 | ✅ 커밋 |
| `android.keystore` | 서명 키스토어 — **외부 백업 필수, 절대 커밋 금지** | ❌ 차단 |
Expand Down
7 changes: 4 additions & 3 deletions apps/twa/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@
"description": "Singcode TWA (Trusted Web Activity) Android build",
"scripts": {
"init": "bubblewrap init --manifest=https://www.singcode.kr/manifest.webmanifest",
"build": "bubblewrap build",
"update": "bubblewrap update"
"build:android": "bubblewrap build && node scripts/verify-target-sdk.mjs",
"update": "bubblewrap update",
"check-sdk": "node scripts/verify-target-sdk.mjs"
},
"devDependencies": {
"@bubblewrap/cli": "^1.24.1"
"@bubblewrap/cli": "^1.25.0"
}
}
95 changes: 95 additions & 0 deletions apps/twa/scripts/verify-target-sdk.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
#!/usr/bin/env node
/**
* 빌드 산출물이 Google Play의 targetSdk 요구사항을 만족하는지 검사한다.
*
* Google Play는 2026-08-31부터 API 36(Android 16) 이상을 타겟하는 앱만 업데이트를 허용한다.
* targetSdkVersion은 Bubblewrap 템플릿에 하드코딩되어 있어 twa-manifest.json으로 제어할 수 없고,
* app/build.gradle은 .gitignore 대상이라 커밋으로 고정할 수도 없다. 즉 이 값은 CLI 버전
* (1.25.0부터 36)과 재생성 여부에만 좌우된다. 이 스크립트는 그 결과가 실제 산출물까지
* 도달했는지 빌드 후 마지막으로 확인하는 안전망이다.
*
* 사용법: node scripts/verify-target-sdk.mjs
*/

import { execFileSync } from 'node:child_process';
import { existsSync, readFileSync, readdirSync } from 'node:fs';
import { homedir } from 'node:os';
import { dirname, join, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';

const REQUIRED_TARGET_SDK = 36;

const twaRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..');
const gradlePath = join(twaRoot, 'app', 'build.gradle');
const manifestPath = join(twaRoot, 'twa-manifest.json');
const apkPath = join(twaRoot, 'app-release-signed.apk');

const log = (message) => console.log(`[target-sdk] ${message}`);
const fail = (message) => {
console.error(`[target-sdk] ❌ ${message}`);
process.exit(1);
};

/** 빌드된 apk의 실제 targetSdk를 aapt2로 읽는다. 도구를 못 찾으면 null. */
function readApkTargetSdk() {
if (!existsSync(apkPath)) return null;

let sdkPath = process.env.ANDROID_HOME;
try {
const config = JSON.parse(readFileSync(join(homedir(), '.bubblewrap', 'config.json'), 'utf8'));
sdkPath = config.androidSdkPath ?? sdkPath;
} catch {
// bubblewrap 설정이 없으면 ANDROID_HOME으로 대체한다.
}
if (!sdkPath) return null;

const buildToolsDir = join(sdkPath, 'build-tools');
if (!existsSync(buildToolsDir)) return null;

const latest = readdirSync(buildToolsDir).sort().pop();
if (!latest) return null;

const aapt2 = join(buildToolsDir, latest, process.platform === 'win32' ? 'aapt2.exe' : 'aapt2');
if (!existsSync(aapt2)) return null;

try {
const badging = execFileSync(aapt2, ['dump', 'badging', apkPath], { encoding: 'utf8' });
return Number(badging.match(/targetSdkVersion:'(\d+)'/)?.[1]);
} catch {
return null;
}
}

if (!existsSync(gradlePath)) {
fail('app/build.gradle을 찾을 수 없습니다. 빌드가 실패했을 수 있습니다.');
}

const gradleSource = readFileSync(gradlePath, 'utf8');
const gradleTargetSdk = Number(gradleSource.match(/targetSdkVersion\s+(\d+)/)?.[1]);
const gradleVersionCode = Number(gradleSource.match(/versionCode\s+(\d+)/)?.[1]);
const { appVersionCode } = JSON.parse(readFileSync(manifestPath, 'utf8'));

if (gradleTargetSdk !== REQUIRED_TARGET_SDK) {
fail(
`app/build.gradle의 targetSdkVersion이 ${gradleTargetSdk}입니다 (필요: ${REQUIRED_TARGET_SDK}).\n` +
` @bubblewrap/cli가 1.25.0 이상인지 확인하고, 빌드 중 재생성 프롬프트에 Yes로 답하세요.`,
);
}

if (gradleVersionCode !== appVersionCode) {
fail(
`versionCode 불일치 — app/build.gradle(${gradleVersionCode}) vs twa-manifest.json(${appVersionCode}).\n` +
` twa-manifest.json을 수정했다면 build 중 재생성 프롬프트에 Yes로 답해 반영하세요.`,
);
}

const apkTargetSdk = readApkTargetSdk();
if (apkTargetSdk === null) {
log('⚠️ aapt2를 찾지 못해 산출물 검증은 건너뜁니다 (app/build.gradle 기준으로는 정상).');
} else if (apkTargetSdk !== REQUIRED_TARGET_SDK) {
fail(`빌드된 apk의 targetSdkVersion이 ${apkTargetSdk}입니다 (필요: ${REQUIRED_TARGET_SDK}).`);
} else {
log(`산출물 검증 통과: apk targetSdkVersion ${apkTargetSdk}.`);
}

log(`✅ targetSdk ${REQUIRED_TARGET_SDK}, versionCode ${appVersionCode} 확인.`);
6 changes: 3 additions & 3 deletions apps/twa/twa-manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@
"path": "./android.keystore",
"alias": "singcode"
},
"appVersionName": "8",
"appVersionCode": 8,
"appVersionName": "12",
"appVersionCode": 12,
"shortcuts": [],
"generatorApp": "bubblewrap-cli",
"webManifestUrl": "https://www.singcode.kr/manifest.webmanifest",
Expand All @@ -44,5 +44,5 @@
"fileHandlers": [],
"launchHandlerClientMode": "",
"displayOverride": [],
"appVersion": "8"
"appVersion": "12"
}
2 changes: 1 addition & 1 deletion apps/web/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "web",
"version": "2.10.0",
"version": "2.11.0",
"type": "module",
"private": true,
"scripts": {
Expand Down
9 changes: 9 additions & 0 deletions apps/web/public/data/changelog.json
Original file line number Diff line number Diff line change
Expand Up @@ -175,5 +175,14 @@
"곡 신고 버튼 문구를 '수정 요청'으로 변경했습니다.",
"검색어 입력 시 버벅이던 현상을 개선했습니다."
]
},
"2.11.0": {
"title": "버전 2.11.0",
"date": "2026-08-13",
"message": [
"인기곡 페이지에 노래방 공식 차트(TJ)를 추가했습니다. 년-월별 순위와 장르별 필터를 지원합니다.",
"곡의 반주 버전(MR, 라이브, 60) 정보를 검색 결과와 인기차트에서 확인할 수 있습니다.",
"곡 카드 디자인을 통일하고 인기차트·최신곡 페이지 UI를 개선했습니다."
]
}
}
6 changes: 3 additions & 3 deletions apps/web/public/sitemap-0.xml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:news="http://www.google.com/schemas/sitemap-news/0.9" xmlns:xhtml="http://www.w3.org/1999/xhtml" xmlns:mobile="http://www.google.com/schemas/sitemap-mobile/1.0" xmlns:image="http://www.google.com/schemas/sitemap-image/1.1" xmlns:video="http://www.google.com/schemas/sitemap-video/1.1">
<url><loc>https://www.singcode.kr/manifest.webmanifest</loc><lastmod>2026-06-11T16:30:10.556Z</lastmod><changefreq>weekly</changefreq><priority>0.7</priority></url>
<url><loc>https://www.singcode.kr</loc><lastmod>2026-06-11T16:30:10.557Z</lastmod><changefreq>weekly</changefreq><priority>0.7</priority></url>
<url><loc>https://www.singcode.kr/patch-notes</loc><lastmod>2026-06-11T16:30:10.557Z</lastmod><changefreq>weekly</changefreq><priority>0.7</priority></url>
<url><loc>https://www.singcode.kr/manifest.webmanifest</loc><lastmod>2026-08-13T08:10:24.253Z</lastmod><changefreq>weekly</changefreq><priority>0.7</priority></url>
<url><loc>https://www.singcode.kr/patch-notes</loc><lastmod>2026-08-13T08:10:24.254Z</lastmod><changefreq>weekly</changefreq><priority>0.7</priority></url>
<url><loc>https://www.singcode.kr</loc><lastmod>2026-08-13T08:10:24.254Z</lastmod><changefreq>weekly</changefreq><priority>0.7</priority></url>
</urlset>
Loading
Loading