Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
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
2 changes: 1 addition & 1 deletion .github/workflows/crawl_recent_tj.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,6 @@ jobs:
echo "SUPABASE_URL=${{ secrets.SUPABASE_URL }}" >> .env
echo "SUPABASE_KEY=${{ secrets.SUPABASE_KEY }}" >> .env

- name: run crawl script
- name: run crawl script - crawlRecentTJ.ts
working-directory: packages/crawling
run: pnpm run recent-tj
43 changes: 43 additions & 0 deletions .github/workflows/tagging_song.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
name: Tagging Songs

on:
# schedule:
# - cron: "0 14 * * *" # 한국 시간 23:00 실행 (UTC+9 → UTC 14:00)
# workflow_dispatch:

Comment on lines +3 to +7
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

1. Workflow has no triggers 🐞 Bug ☼ Reliability

.github/workflows/tagging_song.yml defines on: but all events are commented out, so the workflow
cannot be triggered (and may be treated as invalid by GitHub Actions). This blocks the tagging
pipeline from running at all.
Agent Prompt
### Issue description
The new `tagging_song.yml` workflow has no active triggers because `schedule` and `workflow_dispatch` are commented out under `on:`.

### Issue Context
Without at least one trigger, GitHub Actions will never run this workflow (no scheduled runs and no manual dispatch).

### Fix Focus Areas
- .github/workflows/tagging_song.yml[3-7]

### Suggested change
Enable at least `workflow_dispatch` (and optionally `schedule`) under `on:` so the workflow can run.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

permissions:
contents: write # push 권한을 위해 필요

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

steps:
- name: Checkout branch
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
echo "OPENAI_API_KEY=${{ secrets.OPENAI_API_KEY }}" >> .env

- name: run tagging script - taggingSongs.ts
working-directory: packages/crawling
run: pnpm run tag-songs
2 changes: 1 addition & 1 deletion .github/workflows/update_ky_youtube.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,6 @@ jobs:
echo "SUPABASE_KEY=${{ secrets.SUPABASE_KEY }}" >> .env
echo "OPENAI_API_KEY=${{ secrets.OPENAI_API_KEY }}" >> .env

- name: run update script - packages/crawling/crawlYoutube.ts
- name: run update script - crawlYoutube.ts
working-directory: packages/crawling
run: pnpm run ky-youtube
2 changes: 1 addition & 1 deletion .github/workflows/verify_ky_youtube.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,6 @@ jobs:
echo "SUPABASE_KEY=${{ secrets.SUPABASE_KEY }}" >> .env
echo "OPENAI_API_KEY=${{ secrets.OPENAI_API_KEY }}" >> .env

- name: run verify script - packages/crawling
- name: run verify script - crawlYoutubeVerify.ts
working-directory: packages/crawling
run: pnpm run ky-verify
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ packages/
eslint-config/ — Shared ESLint config (@repo/eslint-config)
format-config/ — Shared Prettier config (@repo/format-config)
typescript-config/ — Shared tsconfig bases
crawling/ — One-off data crawling scripts (not a published package)
crawling/ — Data crawling & tagging scripts (see packages/crawling/CLAUDE.md)
```

## Web App Architecture
Expand Down
88 changes: 35 additions & 53 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

[Singcode - 당신의 노래방 메모장](https://www.singcode.kr)


노래방만 가면 뭘 부르려고 했었지 하면서 부를 곡들을 잊어버린다면. <br/>
매번 인터넷에서 노래방 번호를 검색해야 했었다면. <br/>
내가 어떤 노래를 가장 많이 불렀는지 궁금하다면. <br/>
Expand All @@ -12,14 +11,12 @@ Supabase를 활용한 자체 DB를 통해 금영, TJ 노래방의 번호를 한

<div style="display: flex; justify-content: center; gap: 10px; flex-wrap: wrap;">


</div>


---


## 📦 배포

[Singcode - 당신의 노래방 메모장](https://www.singcode.kr)

## 📚 기술 스택
Expand Down Expand Up @@ -54,109 +51,94 @@ sing-code/
├── package.json # 루트 패키지 관리 파일
└── 기타 설정 파일들 # .gitignore, .prettierrc.json, LICENSE 등
```

### Supabase DB 구조

<div style="display: flex; justify-content: center; gap: 10px; flex-wrap: wrap;">



<img width="700" height="725" alt="image" src="https://github.com/user-attachments/assets/17080dc1-1b63-4cfb-a325-429d207c52d6" />

</div>


## ✨ 주요 기능

### 검색 페이지

* 제목, 가수 이름으로 곡을 검색할 수 있습니다.
- 제목, 가수 이름으로 곡을 검색할 수 있습니다.

<div style="display: flex; justify-content: center; gap: 10px; flex-wrap: wrap;">


</div>

### 검색 페이지 - 재생목록으로 저장

* 기존 재생목록이나 새로운 재생목록에 곡을 저장할 수 있습니다.
### 검색 페이지 - 재생목록으로 저장

<div style="display: flex; justify-content: center; gap: 10px; flex-wrap: wrap;">
- 기존 재생목록이나 새로운 재생목록에 곡을 저장할 수 있습니다.

<div style="display: flex; justify-content: center; gap: 10px; flex-wrap: wrap;">

</div>

### 부를 곡 페이지

* 자신이 저장한 부를 곡을 조회합니다.
* 부를 곡의 순서를 바꿀 수 있습니다.
* 곡을 부르거나, 부르지 않고 삭제할 수 있습니다.

<div style="display: flex; justify-content: center; gap: 10px; flex-wrap: wrap;">
- 자신이 저장한 부를 곡을 조회합니다.
- 부를 곡의 순서를 바꿀 수 있습니다.
- 곡을 부르거나, 부르지 않고 삭제할 수 있습니다.

<div style="display: flex; justify-content: center; gap: 10px; flex-wrap: wrap;">

</div>

* 좋아요 표시한 곡이나 재생목록에 저장한 곡에서 빠르게 부를곡을 추가할 수 있습니다.

- 즐겨찾기 표시한 곡이나 재생목록에 저장한 곡에서 빠르게 부를곡을 추가할 수 있습니다.

<div style="display: flex; justify-content: center; gap: 10px; flex-wrap: wrap;">


</div>


### 인기곡 페이지

* 곡의 추천 순위를 집계해서 보여줍니다.
- 곡의 추천 순위를 집계해서 보여줍니다.

<div style="display: flex; justify-content: center; gap: 10px; flex-wrap: wrap;">


</div>


### 라이브러리 페이지

* 자신의 활동 기록을 조회 및 관리할 수 있습니다.
* 좋아요 한 곡들을 조회하고 일괄 삭제할 수 있습니다.
* 자신이 가장 많이 부른 곡의 순위를 확인할 수 있습니다.
- 자신의 활동 기록을 조회 및 관리할 수 있습니다.
- 즐겨찾기 한 곡들을 조회하고 일괄 삭제할 수 있습니다.
- 자신이 가장 많이 부른 곡의 순위를 확인할 수 있습니다.

<div style="display: flex; justify-content: center; gap: 10px; flex-wrap: wrap;">


</div>

### 출석 체크 기능

* 회원일 경우 하루에 한 번 출석 체크를 통해 포인트를 획득할 수 있습니다. 매일 12시 마다 초기화됩니다.
- 회원일 경우 하루에 한 번 출석 체크를 통해 포인트를 획득할 수 있습니다. 매일 12시 마다 초기화됩니다.

<div style="display: flex; justify-content: center; gap: 10px; flex-wrap: wrap;">


</div>

### 곡 추천 기능

* 출석 체크로 획득한 포인트를 사용해서 곡을 추천할 수 있습니다. 1 포인트 당 1 추천입니다.
- 출석 체크로 획득한 포인트를 사용해서 곡을 추천할 수 있습니다. 1 포인트 당 1 추천입니다.

<div style="display: flex; justify-content: center; gap: 10px; flex-wrap: wrap;">


</div>




### 로그인 & 회원가입 지원

* 몇몇 추가적인 기능을 사용하려면 회원가입을 진행해야 합니다.
* 이메일 인증 회원가입과 카카오 회원가입을 지원합니다.
- 몇몇 추가적인 기능을 사용하려면 회원가입을 진행해야 합니다.
- 이메일 인증 회원가입과 카카오 회원가입을 지원합니다.

<div style="display: flex; justify-content: center; gap: 10px; flex-wrap: wrap;">

</div>


## 📖 프로젝트 기록

- 2025.03.12 : 프로젝트 시작
Expand All @@ -181,29 +163,29 @@ sing-code/
- 2026.2.8 : 버전 2.1.0 배포. 비회원 상태로 곡 부를곡 추가가 가능, Footer 애니메이션 추가.
- 2026.2.20 : 버전 2.2.0 배포. 검색어 자동 완성 기능. es-hangul로 초성 검색 지원.
- 2026.3.2 : 버전 2.3.0 배포. 곡 추천 페이지에서 UI 조정, 곡 추천 기능 추가. 글자 자동 스크롤 기능 추가.

## 📝 회고

### 해결하고자 했던 문제

- 기존 유사한 서비스에서는 하나의 곡을 검색할 때 TJ, 금영 API로 따로따로 곡 데이터를 요청하기에 기존 방식으로는 데이터를 관리하기가 어려웠습니다.
- Supabase로 자체 DB를 구축하고, OPEN API와 puppeteer, cheerio를 활용한 웹 크롤링을 통해 데이터를 파싱하여 DB에 넣어주었습니다.
- OPEN API에 의존하지 않고, 목적에 맞게 활용할 수 있는 자체 API를 구성할 수 있었습니다.
- Supabase로 자체 DB를 구축하고, OPEN API와 puppeteer, cheerio를 활용한 웹 크롤링을 통해 데이터를 파싱하여 DB에 넣어주었습니다.
- OPEN API에 의존하지 않고, 목적에 맞게 활용할 수 있는 자체 API를 구성할 수 있었습니다.
- 52000개가 넘는 DB 테이블에서 모든 검색 결과를 한번에 제공하고 있어서 검색 응답 속도가 굉장히 길었습니다.
- 검색 기능에 Tanstack Query의 useInfiniteQuery를 활용한 무한 스크롤을 도입하여, 페이지 단위의 데이터를 점진적으로 불러오도록 구현했습니다.
- 초기 로딩 속도를 크게 감소시켰고, 사용자 경험을 크게 향상시켰습니다.
- 검색 기능에 Tanstack Query의 useInfiniteQuery를 활용한 무한 스크롤을 도입하여, 페이지 단위의 데이터를 점진적으로 불러오도록 구현했습니다.
- 초기 로딩 속도를 크게 감소시켰고, 사용자 경험을 크게 향상시켰습니다.
- 검색 결과로 렌더링된 곡 컴포넌트에서 이벤트에 따라 UI를 동적으로 변경해야 했으나, 단 하나의 속성값 변경을 위해 매번 queryKey를 무효화하고 전체 데이터를 다시 요청하는 방식은 기술적으로도, 사용자 경험 측면에서도 불필요했습니다.
- `Tanstack Query`의 낙관적 업데이트를 도입하여, UI가 변경해야 하는 이벤트 발생 시 즉각적으로 UI가 변경되도록 최적화하였습니다.
- 로딩 없이 즉시 변경되는 UI를 사용자에게 보여주며 사용자 경험을 크게 개선했습니다.
- `Tanstack Query`의 낙관적 업데이트를 도입하여, UI가 변경해야 하는 이벤트 발생 시 즉각적으로 UI가 변경되도록 최적화하였습니다.
- 로딩 없이 즉시 변경되는 UI를 사용자에게 보여주며 사용자 경험을 크게 개선했습니다.
- 하나의 mutation 요청에 의존하는 side effect(노래를 불렀을 때 의존하는 여러 queryKey 무효화, log 증가 mutation 호출)가 많았기에, 빠르게 여러 mutation을 호출할수록 요청 처리가 늦어지고 timeout이 나오기도 하였습니다.
- mutation 함수에 debounce를 도입하여 짧은 시간 내 많은 요청 시 queryKey를 한 번만 무효화시키게 처리하였습니다.
- Supabase에서 제공하는 Database Functions과 Trigger를 적절하게 활용하여 클라이언트의 요청 응답 속도를 줄였습니다.
- mutation 함수에 debounce를 도입하여 짧은 시간 내 많은 요청 시 queryKey를 한 번만 무효화시키게 처리하였습니다.
- Supabase에서 제공하는 Database Functions과 Trigger를 적절하게 활용하여 클라이언트의 요청 응답 속도를 줄였습니다.
- POST, DELETE, PATCH 요청 때마다 DB와 클라이언트 간의 데이터를 동기화해줘야 했습니다.
- Zustand의 store action으로 제어해보려고 하였으나, 한계를 느끼고 Tanstack Query를 도입하였습니다.
- DB의 변동이 생길 시 queryKey를 무효화시켜 최신 DB 데이터를 가져올 수 있게끔 하였습니다.
- Zustand의 store action으로 제어해보려고 하였으나, 한계를 느끼고 Tanstack Query를 도입하였습니다.
- DB의 변동이 생길 시 queryKey를 무효화시켜 최신 DB 데이터를 가져올 수 있게끔 하였습니다.
- API 요청 시 Supabase DB의 정보가 노출될 위험이 있었습니다.
- Next.js의 API Route를 활용하여 서버리스 API를 구축하고 외부 API의 URL을 감췄습니다.
- Next.js의 API Route를 활용하여 서버리스 API를 구축하고 외부 API의 URL을 감췄습니다.

### 무엇을 얻었는지

- Turborepo를 활용하면서 모노레포의 이해를 높이고 프로젝트에 어떻게 적용하고 확장해야 하는지에 대해 익혔습니다.
Expand Down
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.3.0",
"version": "2.4.0",
"type": "module",
"private": true,
"scripts": {
Expand Down
8 changes: 6 additions & 2 deletions apps/web/public/changelog.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@
"title": "버전 1.2.0",
"message": [
"인기곡 페이지를 추가했습니다.",
"- 전체, 연도별, 월별 부른 곡의 통계와 좋아요 한 곡의 통계를 확인할 수 있습니다.",
"좋아요 한 곡이 통계에 반영되지 않는 문제를 수정했습니다."
"- 전체, 연도별, 월별 부른 곡의 통계와 즐겨찾기 한 곡의 통계를 확인할 수 있습니다.",
"즐겨찾기 한 곡이 통계에 반영되지 않는 문제를 수정했습니다."
]
},
"1.3.0": {
Expand Down Expand Up @@ -108,5 +108,9 @@
"인기곡 페이지의 UI를 개선했습니다. 이제 인기곡 페이지에서 곡을 추천할 수 있습니다.",
"로그인 시 제공되는 코인을 30개로 변경했습니다."
]
},
"2.4.0": {
"title": "버전 2.4.0",
"message": ["네온 나이트 다크 테마를 추가했습니다.", "전체적인 UI를 개선했습니다."]
}
}
2 changes: 1 addition & 1 deletion apps/web/public/sitemap-0.xml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<?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</loc><lastmod>2026-03-25T14:32:28.966Z</lastmod><changefreq>weekly</changefreq><priority>0.7</priority></url>
<url><loc>https://www.singcode.kr</loc><lastmod>2026-04-05T15:20:24.064Z</lastmod><changefreq>weekly</changefreq><priority>0.7</priority></url>
</urlset>
2 changes: 1 addition & 1 deletion apps/web/src/Footer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export default function Footer() {
const navPath = pathname.split('/')[1];

return (
<footer className="bg-background fixed bottom-0 flex h-8 w-full max-w-md justify-between">
<footer className="bg-background fixed bottom-0 flex h-8 w-full max-w-md justify-between border-t">
{navigation.map(item => {
const isActive = '/' + navPath === item.href;
const isAnimating = footerAnimateKey === item.key;
Expand Down
Loading
Loading