From b5871064175296dd8036c5adc52441e252ba8fc5 Mon Sep 17 00:00:00 2001 From: GulSam00 Date: Wed, 25 Mar 2026 23:35:33 +0900 Subject: [PATCH 1/4] =?UTF-8?q?chore=20:=20=EC=9B=8C=ED=81=AC=ED=94=8C?= =?UTF-8?q?=EB=A1=9C=EC=9A=B0=20=EC=BB=A4=EB=A7=A8=EB=93=9C=20=EC=95=88?= =?UTF-8?q?=EC=A0=84=EC=84=B1=20=EB=B0=8F=20=ED=8E=B8=EC=9D=98=EC=84=B1=20?= =?UTF-8?q?=EA=B0=9C=EC=84=A0=20(#168)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - /pr 커맨드: develop/main 브랜치에서 실행 시 차단 로직 추가 - /start 커맨드: 인자 없을 때 git diff 기반 자동 판단 - 워크플로우 사이클에 /pr 단계 추가 Co-Authored-By: Claude Opus 4.6 --- .claude/commands/pr.md | 8 ++++++++ .claude/commands/start.md | 20 ++++++++++++-------- 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/.claude/commands/pr.md b/.claude/commands/pr.md index 29e18aa..dd732e3 100644 --- a/.claude/commands/pr.md +++ b/.claude/commands/pr.md @@ -10,6 +10,14 @@ git branch --show-current ``` + **브랜치가 `develop` 또는 `main` 인 경우 즉시 중단한다.** + 아래 메시지를 출력하고 더 이상 진행하지 않는다. + + ``` + ⛔ `develop` / `main` 브랜치에서는 PR을 생성할 수 없습니다. + `/start <작업 설명>` 으로 이슈를 생성하고 작업 브랜치를 만들어 주세요. + ``` + 2. 브랜치 이름에서 작업 유형과 이슈 번호를 추출한다. - 규칙: `/-` - 예: `feat/42-addSearchFilter` → type=`feat`, issue=`42` diff --git a/.claude/commands/start.md b/.claude/commands/start.md index 14826fa..038c9f9 100644 --- a/.claude/commands/start.md +++ b/.claude/commands/start.md @@ -11,6 +11,10 @@ ## Steps 1. `$ARGUMENTS` 를 분석해 아래 항목을 결정한다. + + **`$ARGUMENTS` 가 비어 있는 경우:** + `git diff` 와 `git status` 로 현재 변경 사항을 파악하고, + 변경 내용을 기반으로 작업 유형, 이슈 제목, 이슈 본문을 자동으로 결정한다. - **작업 유형**: `feat` | `fix` | `hotfix` | `chore` | `refactor` | `doc` - **이슈 제목**: 한국어, 간결하게 - **이슈 본문**: 구현할 내용을 bullet으로 정리 @@ -51,17 +55,17 @@ ### 👉 다음 단계 -| 명령어 | 설명 | -| --------- | ------------------------ | -| `/spsc` | 작업 범위 정의 (권장) | -| `/red` | 테스트 먼저 작성 | -| `/green` | 바로 구현 시작 | +| 명령어 | 설명 | +| -------- | --------------------- | +| `/spsc` | 작업 범위 정의 (권장) | +| `/red` | 테스트 먼저 작성 | +| `/green` | 바로 구현 시작 | ### 🔄 워크플로우 사이클 ``` -일반: /start → /spsc → /red → /green → /refactor → /verify → /commit -단축: /start → /spsc → /green → /verify → /commit +일반: /start → /spsc → /red → /green → /refactor → /verify → /commit → /pr +단축: /start → /spsc → /green → /verify → /commit → /pr ``` - `/red` ~ `/refactor` 사이클은 상황에 따라 생략 가능하다. @@ -72,5 +76,5 @@ ## Notes -- `$ARGUMENTS` 가 비어 있으면 "작업 내용을 입력해주세요." 를 출력하고 중단한다. +- `$ARGUMENTS` 가 비어 있으면 현재 변경 사항(`git diff`, `git status`)을 분석해 자동으로 판단한다. - 이슈 생성 실패 시 에러 메시지를 출력하고 중단한다. From 4927aec089ada03251470b5a982dcde2dc2b3284 Mon Sep 17 00:00:00 2001 From: GulSam00 Date: Wed, 25 Mar 2026 23:35:39 +0900 Subject: [PATCH 2/4] =?UTF-8?q?chore=20:=20crawling=20=EC=9D=BC=EB=B3=B8?= =?UTF-8?q?=EC=96=B4=20=EB=B2=88=EC=97=AD=20=EA=B8=B0=EB=8A=A5=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0=20=EB=B0=8F=20ESLint=20v9=20=EC=84=A4=EC=A0=95=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20(#168)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - postTransDictionary.ts 및 transList.txt 삭제 - TransDictionary 타입, 관련 DB 함수, 로그 함수 제거 - trans 스크립트 제거, CLAUDE.md 반영 - ESLint v9 flat config 추가 및 lint 명령어 수정 Co-Authored-By: Claude Opus 4.6 --- packages/crawling/CLAUDE.md | 28 +++----- packages/crawling/eslint.config.mjs | 4 ++ packages/crawling/package.json | 3 +- packages/crawling/src/assets/transList.txt | 29 -------- .../crawling/src/crawling/crawlRecentTJ.ts | 2 - .../src/crawling/replaceSupabaseFailed.ts | 1 + packages/crawling/src/findKYByOpen.ts | 1 - packages/crawling/src/postTransDictionary.ts | 71 ------------------- packages/crawling/src/supabase/getDB.ts | 29 +------- packages/crawling/src/supabase/postDB.ts | 38 +--------- packages/crawling/src/types.ts | 7 -- packages/crawling/src/updateJpnSongs.ts | 1 - packages/crawling/src/utils/logData.ts | 20 ------ 13 files changed, 16 insertions(+), 218 deletions(-) create mode 100644 packages/crawling/eslint.config.mjs delete mode 100644 packages/crawling/src/assets/transList.txt delete mode 100644 packages/crawling/src/postTransDictionary.ts diff --git a/packages/crawling/CLAUDE.md b/packages/crawling/CLAUDE.md index 735f568..7136ffc 100644 --- a/packages/crawling/CLAUDE.md +++ b/packages/crawling/CLAUDE.md @@ -13,7 +13,6 @@ pnpm ky-open # Open API(금영)로 KY 번호 수집 pnpm ky-youtube # YouTube 크롤링으로 KY 번호 수집 + AI 검증 pnpm ky-verify # 기존 KY 번호의 실제 존재 여부 재검증 (체크포인트 지원) pnpm ky-update # ky-youtube + ky-verify 병렬 실행 -pnpm trans # 일본어 아티스트명 → 한국어 번역 후 DB 저장 pnpm test # vitest 실행 pnpm lint # ESLint ``` @@ -74,24 +73,14 @@ findKYByOpen.ts └─ 제목 + 아티스트 문자열 비교로 KY 번호 매칭 ``` -**일본어 번역** - -``` -postTransDictionary.ts - └─ getSongsJpnDB() # 일본어 포함된 곡 필터링 - └─ transChatGPT() # GPT-4-turbo로 아티스트명 번역 - └─ postTransDictionariesDB() # trans_dictionaries 테이블에 저장 -``` - ### 핵심 패턴: 진행 상태 저장 (체크포인트) 장시간 실행되는 스크립트가 중단됐을 때 재시작하면 처음부터 다시 하지 않도록, `src/assets/`에 텍스트 파일로 진행 상태를 기록한다. -| 파일 | 용도 | -| ----------------------------------------- | ---------------------------------- | -| `src/assets/transList.txt` | 이미 번역 시도한 일본어 아티스트명 | -| `src/assets/crawlKYValidList.txt` | 검증 완료된 (제목-아티스트) 쌍 | -| `src/assets/crawlKYYoutubeFailedList.txt` | YouTube 크롤링 실패 목록 | +| 파일 | 용도 | +| ----------------------------------------- | ------------------------------ | +| `src/assets/crawlKYValidList.txt` | 검증 완료된 (제목-아티스트) 쌍 | +| `src/assets/crawlKYYoutubeFailedList.txt` | YouTube 크롤링 실패 목록 | `logData.ts`의 `save*` / `load*` 함수로 관리. 스크립트 시작 시 로드해 `Set`으로 변환 후 O(1) 검색으로 스킵 처리. @@ -101,11 +90,10 @@ postTransDictionary.ts ### Supabase 테이블 -| 테이블 | 용도 | -| -------------------- | -------------------------------- | -| `songs` | 메인 곡 데이터 (TJ/KY 번호 포함) | -| `invalid_ky_songs` | KY 번호 수집 실패 목록 | -| `trans_dictionaries` | 일본어 → 한국어 번역 사전 | +| 테이블 | 용도 | +| ------------------ | -------------------------------- | +| `songs` | 메인 곡 데이터 (TJ/KY 번호 포함) | +| `invalid_ky_songs` | KY 번호 수집 실패 목록 | ### AI 유틸 diff --git a/packages/crawling/eslint.config.mjs b/packages/crawling/eslint.config.mjs new file mode 100644 index 0000000..dc943dc --- /dev/null +++ b/packages/crawling/eslint.config.mjs @@ -0,0 +1,4 @@ +import { config } from '@repo/eslint-config/base'; + +/** @type {import("eslint").Linter.Config} */ +export default config; diff --git a/packages/crawling/package.json b/packages/crawling/package.json index cfa6209..e81f2ff 100644 --- a/packages/crawling/package.json +++ b/packages/crawling/package.json @@ -11,9 +11,8 @@ "ky-youtube": "tsx src/crawling/crawlYoutube.ts", "ky-verify": "tsx src/crawling/crawlYoutubeVerify.ts", "ky-update": "pnpm run ky-youtube & pnpm run ky-verify", - "trans": "tsx src/postTransDictionary.ts", "recent-tj": "tsx src/crawling/crawlRecentTJ.ts", - "lint": "eslint . --ext .ts,.js", + "lint": "eslint .", "test": "vitest run", "format": "prettier --write \"**/*.{ts,tsx,md}\"" }, diff --git a/packages/crawling/src/assets/transList.txt b/packages/crawling/src/assets/transList.txt deleted file mode 100644 index 486a91c..0000000 --- a/packages/crawling/src/assets/transList.txt +++ /dev/null @@ -1,29 +0,0 @@ -鳥羽一郎 -欅坂46 -张芸京 -中島みゆき -双笙 -藤咲かりん(miko) -吉田羊 -紫今 -友成空 -文夫 -米津玄師 -林俊杰,林俊峰 -Ayase,R-指定 -张朕,婉枫 -ヨルシカ -矢口真里とストローハット -福田こうへい -彩菜 -桑田佳祐 -嵐 -福原遥 -ラックライフ -いきものがかり -ユンナ -梁咏琪 -ポルノグラフィティ -氷川きよし -薛明媛,朱贺 -陈泫孝(大泫) diff --git a/packages/crawling/src/crawling/crawlRecentTJ.ts b/packages/crawling/src/crawling/crawlRecentTJ.ts index 9cfbf65..8238714 100644 --- a/packages/crawling/src/crawling/crawlRecentTJ.ts +++ b/packages/crawling/src/crawling/crawlRecentTJ.ts @@ -60,6 +60,4 @@ console.log('실패 개수 : ', result.failed.length); console.log('성공 데이터 : ', result.success); console.log('실패 데이터 : ', result.failed); - - await browser.close(); diff --git a/packages/crawling/src/crawling/replaceSupabaseFailed.ts b/packages/crawling/src/crawling/replaceSupabaseFailed.ts index 3d9a163..cdf2224 100644 --- a/packages/crawling/src/crawling/replaceSupabaseFailed.ts +++ b/packages/crawling/src/crawling/replaceSupabaseFailed.ts @@ -1,6 +1,7 @@ import { getSongsKyNullDB } from '@/supabase/getDB'; import { postInvalidKYSongsDB } from '@/supabase/postDB'; import { Song } from '@/types'; + // import { loadCrawlYoutubeFailedKYSongs } from '@/utils/logData'; const data: Song[] = await getSongsKyNullDB(); diff --git a/packages/crawling/src/findKYByOpen.ts b/packages/crawling/src/findKYByOpen.ts index 5177f39..aaadb8c 100644 --- a/packages/crawling/src/findKYByOpen.ts +++ b/packages/crawling/src/findKYByOpen.ts @@ -76,4 +76,3 @@ console.log(` - 성공: ${resultsLog.success.length}곡 - 실패: ${resultsLog.failed.length}곡 `); - diff --git a/packages/crawling/src/postTransDictionary.ts b/packages/crawling/src/postTransDictionary.ts deleted file mode 100644 index 7bcb59e..0000000 --- a/packages/crawling/src/postTransDictionary.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { sleep } from 'openai/core'; - -import { getSongsJpnDB, getTransDictionariesDBByOriginal } from '@/supabase/getDB'; -import { postTransDictionariesDB } from '@/supabase/postDB'; -import { TransDictionary, TransSong } from '@/types'; -// import { loadDictionariesLog, saveDictionariesLog } from '@/utils/logData'; -import { transChatGPT } from '@/utils/transChatGPT'; - -const data: TransSong[] = await getSongsJpnDB(); -console.log('data to translate : ', data.length); - -// 만약 null로 반환된다면 해당 id와 함께 배열에 담가두다가 끝났을 때 error.txt에 저장 - -const unknownData: { item: TransSong; error: any }[] = []; - -const transData: TransDictionary[] = []; - -const refreshData = async () => { - console.log('refreshData'); - - await postTransDictionariesDB(transData); - // for (const song of transData) { - // saveDictionariesLog(song.original_japanese); - // } - - transData.length = 0; - unknownData.length = 0; -}; - -let count = 0; - -// const tryLogs = loadDictionariesLog(); - -for (const song of data) { - if (count >= 10) { - await refreshData(); - count = 0; - } - console.log('count : ', count++); - await sleep(150); // 0.15초(150ms) 대기 - - // if (tryLogs.has(song.artist)) { - // continue; - // } - - const dupArtistTrans = await getTransDictionariesDBByOriginal(song.artist); - - if (dupArtistTrans) { - // saveDictionariesLog(song.artist); - continue; - } - - if (song.isArtistJp) { - const artistTrans = await transChatGPT(song.artist); - if (!artistTrans || artistTrans.length === 0) { - unknownData.push({ item: song, error: 'transChatGPT failed' }); - transData.push({ - original_japanese: song.artist, - translated_korean: null, - }); - } else { - console.log(song.artist, artistTrans); - transData.push({ - original_japanese: song.artist, - translated_korean: artistTrans, - }); - } - } -} - -refreshData(); diff --git a/packages/crawling/src/supabase/getDB.ts b/packages/crawling/src/supabase/getDB.ts index abeb58e..81c7a3c 100644 --- a/packages/crawling/src/supabase/getDB.ts +++ b/packages/crawling/src/supabase/getDB.ts @@ -1,4 +1,4 @@ -import { TransDictionary, TransSong } from '@/types'; +import { TransSong } from '@/types'; import { containsJapanese } from '@/utils/parseString'; import { getClient } from './getClient'; @@ -63,33 +63,6 @@ export async function getSongsKyNotNullDB(max: number = 50000) { return data; } -export async function getTransDictionariesDB(): Promise { - const supabase = getClient(); - - // artist 정렬 - const { data, error } = await supabase.from('trans_dictionaries').select('*'); - - if (error) throw error; - - return data; -} -export async function getTransDictionariesDBByOriginal( - original: string, -): Promise { - const supabase = getClient(); - - // artist 정렬 - const { data, error } = await supabase - .from('trans_dictionaries') - .select('*') - .eq('original_japanese', original) - .limit(1); - - if (error) throw error; - - return data[0] ?? null; -} - export async function getInvalidKYSongsDB(): Promise< { id: string; title: string; artist: string }[] > { diff --git a/packages/crawling/src/supabase/postDB.ts b/packages/crawling/src/supabase/postDB.ts index 72e5cc2..d53f82d 100644 --- a/packages/crawling/src/supabase/postDB.ts +++ b/packages/crawling/src/supabase/postDB.ts @@ -1,4 +1,4 @@ -import { LogData, Song, TransDictionary } from '@/types'; +import { LogData, Song } from '@/types'; import { getClient } from './getClient'; @@ -36,42 +36,6 @@ export async function postSongsDB(songs: Song[] | Song) { return results; } -export async function postTransDictionariesDB(dictionaries: TransDictionary[]) { - const supabase = getClient(); - - const results: LogData = { - success: [] as TransDictionary[], - failed: [] as { item: TransDictionary; error: any }[], - }; - - // 각 곡을 개별적으로 처리 - for (const item of dictionaries) { - try { - const { original_japanese, translated_korean } = item; - const { data, error } = await supabase - .from('trans_dictionaries') - .insert([{ original_japanese, translated_korean }]) - .select(); - - if (error) { - results.failed.push({ item, error }); - } else { - results.success.push(item); - } - } catch (error) { - results.failed.push({ item, error }); - } - } - - console.log(` - 총 ${dictionaries.length} 데이터 중: - - 성공: ${results.success.length}개 - - 실패: ${results.failed.length}개 - `); - - return results; -} - export async function postVerifyKySongsDB(song: Song) { const supabase = getClient(); diff --git a/packages/crawling/src/types.ts b/packages/crawling/src/types.ts index d403d15..1534694 100644 --- a/packages/crawling/src/types.ts +++ b/packages/crawling/src/types.ts @@ -21,13 +21,6 @@ export interface TransSong extends Song { type?: 'title' | 'artist'; } -export interface TransDictionary { - id?: string; - original_japanese: string; - translated_korean: string | null; - created_at?: string; -} - export interface LogData { success: T[]; failed: { item: T; error: any }[]; diff --git a/packages/crawling/src/updateJpnSongs.ts b/packages/crawling/src/updateJpnSongs.ts index a43a5de..8d9c222 100644 --- a/packages/crawling/src/updateJpnSongs.ts +++ b/packages/crawling/src/updateJpnSongs.ts @@ -53,4 +53,3 @@ for (const song of transData) { updateSongsJpnDB(song); } } - diff --git a/packages/crawling/src/utils/logData.ts b/packages/crawling/src/utils/logData.ts index e326647..b26a40e 100644 --- a/packages/crawling/src/utils/logData.ts +++ b/packages/crawling/src/utils/logData.ts @@ -1,26 +1,6 @@ import fs from 'fs'; import path from 'path'; -export function saveDictionariesLog(japanese: string) { - const logPath = path.join('src', 'assets', 'transList.txt'); - const logDir = path.dirname(logPath); - if (!fs.existsSync(logDir)) { - fs.mkdirSync(logDir, { recursive: true }); - } - fs.appendFileSync(logPath, `${japanese}\n`, 'utf-8'); -} - -export function loadDictionariesLog(): Set { - const logPath = path.join('src', 'assets', 'transList.txt'); - if (!fs.existsSync(logPath)) return new Set(); - const lines = fs - .readFileSync(logPath, 'utf-8') - .split('\n') - .map(line => line.trim()) - .filter(Boolean); - return new Set(lines); -} - export function loadCrawlYoutubeFailedKYSongs(): Set { const logPath = path.join('src', 'assets', 'crawlKYYoutubeFailedList.txt'); if (!fs.existsSync(logPath)) return new Set(); From 1cf103c2572383a953b530d069144f757bf3fb38 Mon Sep 17 00:00:00 2001 From: GulSam00 Date: Wed, 25 Mar 2026 23:35:55 +0900 Subject: [PATCH 3/4] =?UTF-8?q?chore=20:=20apps/web=20CLAUDE.md=20?= =?UTF-8?q?=ED=8F=AC=EB=A7=B7=20=EC=A0=95=EB=A6=AC=20(#168)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.6 --- apps/web/CLAUDE.md | 32 ++++++++++++++++++-------------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/apps/web/CLAUDE.md b/apps/web/CLAUDE.md index e39ab9d..f784765 100644 --- a/apps/web/CLAUDE.md +++ b/apps/web/CLAUDE.md @@ -25,6 +25,7 @@ No test suite is configured. ### Monorepo Structure This is a pnpm workspace monorepo. The web app lives at `apps/web/`. Key workspace packages: + - `@repo/open-api` — wrapper around the external karaoke open API (used in API routes) - `@repo/query` — shared TanStack Query setup - `@repo/eslint-config`, `@repo/format-config` — shared tooling configs @@ -49,6 +50,7 @@ This is a pnpm workspace monorepo. The web app lives at `apps/web/`. Key workspa ### Supabase Client Variants Three different Supabase clients for different contexts: + - `src/lib/supabase/client.ts` — browser client (`createBrowserClient`), uses `NEXT_PUBLIC_` env vars - `src/lib/supabase/server.ts` — server/route handler client (`createServerClient`) - `src/lib/supabase/api.ts` — legacy API routes client (Next.js Pages-style `req/res`) @@ -87,6 +89,7 @@ Song searches go through `GET /api/open_songs/[type]/[param]` which proxies to ` ## Environment Variables Required in `.env` / `.env.development.local`: + - `NEXT_PUBLIC_SUPABASE_URL` - `NEXT_PUBLIC_SUPABASE_ANON_KEY` - `SUPABASE_URL` (server-only, used in legacy API client) @@ -99,12 +102,12 @@ Required in `.env` / `.env.development.local`: Format: `/` -| type | usage | -|------|-------| -| `feat` | new feature | -| `fix` | bug fix | -| `hotfix` | urgent fix | -| `chore` | maintenance, docs, config | +| type | usage | +| --------- | ------------------------------ | +| `feat` | new feature | +| `fix` | bug fix | +| `hotfix` | urgent fix | +| `chore` | maintenance, docs, config | | `release` | release (e.g. `release/2.1.0`) | The part after the slash uses camelCase (e.g. `feat/scrollText`, `feat/FooterNavbar`, `fix/loginAuth`). @@ -115,16 +118,17 @@ Branch flow: `feat/*` → `develop` → `main` Format: ` : ` — one space before and after the colon. -| type | usage | -|------|-------| -| `feat` | new feature | -| `fix` | bug fix | -| `hotfix` | urgent bug fix | -| `chore` | version bump, config, format, cleanup | -| `refactor` | refactoring | -| `doc` | documentation | +| type | usage | +| ---------- | ------------------------------------- | +| `feat` | new feature | +| `fix` | bug fix | +| `hotfix` | urgent bug fix | +| `chore` | version bump, config, format, cleanup | +| `refactor` | refactoring | +| `doc` | documentation | Examples: + ``` feat : MarqueeText 자동 스크롤 텍스트 적용 fix : SongCard css 수정 From 1cf69042d3b128278c719d3ea420d9b536b1cf7c Mon Sep 17 00:00:00 2001 From: GulSam00 Date: Wed, 25 Mar 2026 23:41:54 +0900 Subject: [PATCH 4/4] =?UTF-8?q?chore=20:=20gitignore=EC=97=90=20settings.l?= =?UTF-8?q?ocal.json=20=EC=B6=94=EA=B0=80=20=EB=B0=8F=20sitemap=20?= =?UTF-8?q?=EA=B0=B1=EC=8B=A0=20(#168)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.6 --- .gitignore | 4 +++- apps/web/public/sitemap-0.xml | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index a4dd6ba..2d74ab1 100644 --- a/.gitignore +++ b/.gitignore @@ -48,4 +48,6 @@ yarn-error.log* # Claude Code local settings (contains secrets) temp/ -.vscode \ No newline at end of file +.vscode + +settings.local.json \ No newline at end of file diff --git a/apps/web/public/sitemap-0.xml b/apps/web/public/sitemap-0.xml index 99ebb14..73fd5f7 100644 --- a/apps/web/public/sitemap-0.xml +++ b/apps/web/public/sitemap-0.xml @@ -1,4 +1,4 @@ -https://www.singcode.kr2026-03-02T07:59:31.054Zweekly0.7 +https://www.singcode.kr2026-03-25T14:32:28.966Zweekly0.7 \ No newline at end of file