Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
210c2fb
[CHORE] 팀 구성원 변경에 따른 Auto PR 스크립트 수정 (#299)
i-meant-to-be Jul 2, 2025
288e661
[CHORE] GitHub 저장소 이슈 목록을 Notion으로 동기화 (#303)
i-meant-to-be Jul 5, 2025
3133517
[DESIGN] 로그인/로그아웃 버튼 디자인 변경 (#300)
i-meant-to-be Jul 7, 2025
c1e6d9e
[REFACTOR] TimerPage 비즈니스 로직 분리 (#304)
jaeml06 Jul 11, 2025
eac5139
[FIX] 타이머 키보드 오류 수정 (#315)
i-meant-to-be Jul 13, 2025
f77cc18
[FEAT] 타임박스 복사 기능 구현 (#316)
jaeml06 Jul 14, 2025
bb50b75
[REFACTOR] Axios 코드 및 API 오류 처리 보강 (#324)
i-meant-to-be Jul 22, 2025
8be1711
[FIX] 버튼 "따닥"으로 인한 중복 호출로 시간표가 여러 개 생기는 문제 해결 (#321)
useon Jul 22, 2025
e43e20c
[FIX] 타이머에서 숫자 키패드 Enter가 동작하지 않는 문제 수정 (#328)
i-meant-to-be Jul 24, 2025
faf0b2c
[DESIGN] 비동기 처리 관련 컴포넌트 개선 (#325)
i-meant-to-be Jul 24, 2025
32bbe17
[FIX] 타이머 좌우 방향키 조작 비활성화 (#333)
i-meant-to-be Jul 27, 2025
bd32663
[FEAT] 커스텀 타종 설정 추가 (#326)
jaeml06 Aug 1, 2025
ac32448
[REFACTOR] 비동기 처리 로직 보강 (#329)
i-meant-to-be Aug 1, 2025
7cc2c69
[FIX] 비회원으로 로그인 하지 않고 뒤로 가기를 눌렀을 때 에러 발생 문제 해결 (#330)
useon Aug 4, 2025
dd8fde7
[FIX] 상대팀이 모두 시간 소진, 2회 이상 발언할 시간이 남아있을 때, 1회 발언 이후, 전체시간을 전부 소비할 수 없…
jaeml06 Aug 5, 2025
cb3744f
[FIX] 발언자 이름 출력 조건이 반대로 되어 있는 문제 수정
i-meant-to-be Aug 10, 2025
b6d444d
[DESIGN] 디자인 시스템 적용 (#335)
i-meant-to-be Aug 13, 2025
e228700
[FEAT] 코인 토스 기능 구현 (#339)
useon Aug 17, 2025
e6d68d9
[FIX] 일부 오류 수정 (#341)
i-meant-to-be Aug 22, 2025
30e0147
[REFACTOR] 타종 소리를 횟수에 따른 3개의 정적 파일로 변경 (#344)
jaeml06 Aug 26, 2025
666980d
[DOCS] README.md 파일에 프로젝트 설명 추가 (#346)
jaeml06 Aug 26, 2025
bdd83cf
[DESIGN] 사소한 디자인 개선 사항 적용 (#351)
i-meant-to-be Aug 28, 2025
1f8c384
[FIX] 타이머 오류 해결 (#350)
i-meant-to-be Aug 28, 2025
9589dc3
[DESIGN] 종소리 설정에 아코디언 UI 적용 (#359)
i-meant-to-be Sep 6, 2025
c7bc37a
[FIX] 시간표 수정 화면과 레이아웃의 오류 해결 (#352)
i-meant-to-be Sep 6, 2025
b7af2ad
[FIX] 종소리 관련 오류 수정 (#360)
i-meant-to-be Sep 6, 2025
af2e52b
[FEAT] 메인 페이지 디자인 변경 및 토론 템플릿 섹션 추가 (#356)
jaeml06 Sep 6, 2025
df5e356
[FEAT] 피드백 타이머 구현 (#361)
useon Sep 7, 2025
58a16a0
[FEAT] 토론종료화면 구현 (#363)
useon Sep 7, 2025
fce40fc
[FIX] 코인토스 플로우 추가 및 버그 해결 (#364)
useon Sep 7, 2025
a18d6a7
fix: 토론 템플릿 데이터 수정 (#366)
jaeml06 Sep 7, 2025
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
3 changes: 2 additions & 1 deletion .github/workflows/Auto_PR_Setting.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,8 @@ jobs:
LABELS=$(echo "${ISSUE_DATA}" | jq -r '.labels | join(",")')

# 팀 멤버 목록을 정의하고, 할당되지 않은 멤버를 리뷰어로 추가.
TEAM_MEMBERS=("jaeml06" "i-meant-to-be" "eunwoo-levi" "katie424")
# 단, 현재 활동하지 않는 멤버는 제외. (현재 비활성 멤버 = 엘, 케이티)
TEAM_MEMBERS=("jaeml06" "i-meant-to-be" "useon")
IFS=', ' read -r -a ASSIGNEE_ARRAY <<< "${ASSIGNEES}"
REVIEWERS=()
for MEMBER in "${TEAM_MEMBERS[@]}"; do
Expand Down
32 changes: 32 additions & 0 deletions .github/workflows/Notion_Sync.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
name: Notion Sync

on:
issues:
types: [opened]
pull_request:
types: [opened, reopened, closed]
pull_request_target: # For accessing secrets from forked repos
types: [opened, reopened, closed]

jobs:
sync:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.x'

- name: Install dependencies
run: pip install requests pytz

- name: Run Notion Sync Script
env:
NOTION_TOKEN: ${{ secrets.NOTION_TOKEN }}
NOTION_DATABASE_ID: ${{ secrets.NOTION_DATABASE_ID }}
NOTION_USER_UUID_MAP: ${{ secrets.NOTION_USER_UUID_MAP }}
GITHUB_EVENT_PATH: ${{ github.event_path }}
run: python ./notion_sync.py
1 change: 1 addition & 0 deletions .github/workflows/deploy-development.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ on:

jobs:
build-and-deploy:
if: github.event_name != 'pull_request' || github.event.pull_request.merged == true
runs-on: ubuntu-latest
environment: DEPLOY_DEV

Expand Down
1 change: 1 addition & 0 deletions .github/workflows/deploy-production.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ on:

jobs:
build-and-deploy:
if: github.event_name != 'pull_request' || github.event.pull_request.merged == true
runs-on: ubuntu-latest
environment: DEPLOY_PROD

Expand Down
12 changes: 12 additions & 0 deletions .storybook/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,17 @@ const config: StorybookConfig = {
core: {
builder: '@storybook/builder-vite',
},
async viteFinal(config, { configType }) {
console.log(`# configType = ${configType}`);

// Storybook의 실행 모드를 테스트 모드로 고정하여
// msw가 잘 동작할 수 있게 준비
config.define = {
...config.define,
'import.meta.env.MODE': JSON.stringify('test'),
};

return config;
},
};
export default config;
85 changes: 83 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,83 @@
# debate-timer-fe
디베이트 타이머 프론트엔드 레포지토리입니다
# ⏲️ 디베이트 타이머: Debate Timer (Front-end)

디베이트 타이머의 Front-end 저장소입니다.
우리는 토론 진행을 효율적으로 돕는 서비스를 제공하는 것을 목표로 토론에 진행에 필요한 다양한 형식의 타이머를 제공해요.
전체 프로젝트 및 서비스 소개는 [조직 메인 페이지](https://github.com/debate-timer)에서 확인하세요.

[서비스 바로가기](https://www.debate-timer.com)

## 🛠️ 기술 스택

| 항목 | 스택 |
| ------------- | ------------------------------ |
| Core | React, TypeScript |
| 빌드 | Vite |
| 스타일링 | TailwindCSS v3 |
| 테스트 | Vitest, Storybook, MSW |
| 네트워킹 | Axios, TanStack Query |
| 린팅 / 포매팅 | ESLint, StyleLint, Prettier |
| CI / CD | S3, CloudFront, Github Actions |
| 분석 | GA4 |

---

## 실행 방법

```bash
사전 준비
Node.js: v20.x LTS
패키지 매니저: npm

# 의존성 설치
npm install

# 개발 서버 실행
npm run dev

# 테스트 실행
npm run test

# Storybook 실행
npm run storybook

# 빌드
npm run build
```

---

## 배포

- AWS S3 + CloudFront를 사용하여 웹사이트 호스팅
- GitHub Actions를 이용해 main 브랜치에 푸시 시 자동 배포

---

## 브랜치 전략

- main: 배포용 안정화 브랜치
- develop: 개발 중인 기능 통합 브랜치
- feature/\*: 각 기능별 작업 브랜치

---

## 기여자

### 현재 활동 중

| **썬데이** | **치코** | **숀** |
| :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: |
| <a href="https://github.com/useon" target="_blank"><img src="https://avatars.githubusercontent.com/u/74897720?v=4" width="150" height="150" style="border-radius: 50%;" alt="썬데이"/></a> | <a href="https://github.com/jaeml06" target="_blank"><img src="https://avatars.githubusercontent.com/u/107801932?v=4" width="150" height="150" style="border-radius: 50%;" alt="치코"/></a> | <a href="https://github.com/i-meant-to-be" target="_blank"><img src="https://avatars.githubusercontent.com/u/77564014?v=4" width="150" height="150" style="border-radius: 50%;" alt="숀"/></a> |

### 휴식 중

| **엘** | **케이티** |
| :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: |
| <a href="https://github.com/eunwoo-levi" target="_blank"><img src="https://avatars.githubusercontent.com/u/162898956?v=4" width="150" height="150" style="border-radius: 50%;" alt="엘"/></a> | <a href="https://github.com/katie424" target="_blank"><img src="https://avatars.githubusercontent.com/u/80771814?v=4" width="150" height="150" style="border-radius: 50%;" alt="케이티"/></a> |

---

## 추가 링크

- Backend Repository: <https://github.com/debate-timer/debate-timer-be>
- Debate Timer Organization: <https://github.com/debate-timer>
230 changes: 230 additions & 0 deletions notion_sync.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
import os
import json
import re
import datetime
import requests
import pytz

# Import envs
NOTION_TOKEN = os.getenv("NOTION_TOKEN")
NOTION_DATABASE_ID = os.getenv("NOTION_DATABASE_ID")
NOTION_USER_UUID_MAP_STR = os.getenv("NOTION_USER_UUID_MAP")
NOTION_API_URL = "https://api.notion.com/v1"

# Parse GitHub ID-Notion UUID map
if NOTION_USER_UUID_MAP_STR:
NOTION_USER_UUID_MAP = json.loads(NOTION_USER_UUID_MAP_STR)
else:
print("NOTION_USER_UUID_MAP secret not found or empty.")
NOTION_USER_UUID_MAP = {}

# Set header for HTTP request
HEADERS = {
"Authorization": f"Bearer {NOTION_TOKEN}",
"Notion-Version": "2022-06-28",
"Content-Type": "application/json",
}

# Load GitHub event payload
def get_github_event_payload():
event_path = os.getenv("GITHUB_EVENT_PATH")
if not event_path:
print("GITHUB_EVENT_PATH is not valid.")
return None
try:
with open(event_path, "r") as f:
return json.load(f)
except (FileNotFoundError, json.JSONDecodeError) as e:
print(f"Failed to read event payload: {e}")
return None

# Find Notion DB item by GitHub issue id
def find_notion_page_by_github_id(github_id):
query_url = f"{NOTION_API_URL}/databases/{NOTION_DATABASE_ID}/query"
payload = {
"filter": {
"property": "ID",
"number": {
"equals": github_id
}
}
}
response = requests.post(query_url, headers=HEADERS, json=payload)
response.raise_for_status()
results = response.json().get("results")
return results[0] if results else None

# Create new Notion DB item
def create_notion_page(title, status, github_assignee_id, github_url, github_id):
create_url = f"{NOTION_API_URL}/pages"

# Parse Notion user id
notion_user_id = NOTION_USER_UUID_MAP.get(github_assignee_id, "") if github_assignee_id else ""

# Parse current time (KST)
korea_timezone = pytz.timezone('Asia/Seoul')
current_datetime = datetime.datetime.now(korea_timezone)
current_datetime_str = current_datetime.isoformat()

data = {
"parent": {"database_id": NOTION_DATABASE_ID},
"properties": {
"이름": {
"title": [
{
"text": {
"content": title
}
}
]
},
"URL": {
"url": github_url
},
"상태": {
"select": {
"name": status
}
},
"ID": {
"number": github_id
},
"생성 날짜": {
"date": {
"start": current_datetime_str
}
}
}
}
if (notion_user_id):
data["properties"]["작업자"] = {
"people": [
{
"id": notion_user_id
}
]
}

response = requests.post(create_url, headers=HEADERS, json=data)
response.raise_for_status()
print(f"Notion item created / Title: '{title}', Status: '{status}'")
return response.json()

# Patch Notion DB item
def update_notion_page_status(page_id, status):
update_url = f"{NOTION_API_URL}/pages/{page_id}"
data = {
"properties": {
"상태": {
"select": {
"name": status
}
}
}
}
response = requests.patch(update_url, headers=HEADERS, json=data)
response.raise_for_status()
print(f"Updated status of Notion page '{page_id}' as '{status}'")
return response.json()

# Handle issue-related event
def handle_issue_event(payload):
issue = payload.get("issue")
if not issue:
print("Issue data is not in payload.")
return

action = payload.get("action")
issue_title = issue.get("title")
issue_url = issue.get("html_url")
issue_number = issue.get("number")
issue_assignees = issue.get("assignees", [])
issue_assignee_id = issue_assignees[0].get("id") if issue_assignees else None

if action == "opened":
print(f"Issue opened: #{issue_number} - {issue_title}")
# Check duplication
notion_page = find_notion_page_by_github_id(issue_number)
if not notion_page:
create_notion_page(issue_title, "열림", issue_assignee_id, issue_url, issue_number)
else:
print(f"Issue #{issue_number} is already in Notion DB. This task will be skipped.")
else:
print(f"Not supported action: {action}")

# Handle PR event
def handle_pull_request_event(payload):
pr = payload.get("pull_request")
if not pr:
print("PR data is not in payload.")
return

action = payload.get("action")
pr_title = pr.get("title")
pr_number = pr.get("number")
pr_url = pr.get("html_url")
pr_assignees = pr.get("assignees", [])
pr_assignee_id = pr_assignees[0].get("id") if pr_assignees else None

# Find all related issue ID
body = pr.get("body", "")
issue_ids = re.findall(r"(?:close|closes|closed|fix|fixes|fixed)\s+#(\d+)", body, re.IGNORECASE)
issue_ids = [int(num) for num in issue_ids]

# If no specified issue, create Notion DB item with PR id
if not issue_ids:
print(f"Cannot find related issue numbers in PR #{pr_number}. Add PR id itself on the list.")
issue_ids = [pr_number]

for related_issue_number in issue_ids:
notion_page = find_notion_page_by_github_id(related_issue_number)

if not notion_page:
# There is no related issue on Notion or have to add PR itself
if related_issue_number == pr_number: # Register PR itself
print(f"Add PR #{pr_number} on the Notion DB.")
create_notion_page(f"PR: {pr_title}", "병합 요청됨", pr_assignee_id, pr_url, pr_number)
else:
print(f"Issue #{related_issue_number} is not in Notion DB. Create new one.")
create_notion_page(f"이슈 #{related_issue_number} (PR 연동)", "병합 요청됨", pr_assignee_id, pr_url, related_issue_number)
continue

page_id = notion_page["id"]

if action == "opened" or action == "reopened":
print(f"PR opened/reopened: #{pr_number} - {pr_title}. Change status of related issue #{related_issue_number}.")
update_notion_page_status(page_id, "병합 요청됨")
elif action == "closed":
if pr.get("merged"):
print(f"PR merged: #{pr_number} - {pr_title}. Change status of related issue #{related_issue_number}.")
update_notion_page_status(page_id, "병합됨")
else:
print(f"PR closed (not merged): #{pr_number} - {pr_title}. Change status of related issue #{related_issue_number}.")
update_notion_page_status(page_id, "닫힘")
else:
print(f"Not supported PR action: {action}")

if __name__ == "__main__":
event_payload = get_github_event_payload()

if not event_payload:
print("Cannot read event payload. Terminate.")
else:
event_name = os.getenv("GITHUB_EVENT_NAME")
print(f"GitHub Event Name: {event_name}")

try:
if event_name == "issues":
handle_issue_event(event_payload)
elif event_name == "pull_request" or event_name == "pull_request_target":
handle_pull_request_event(event_payload)
else:
print(f"Not supported GitHub event: {event_name}")
except requests.exceptions.RequestException as e:
print(f"Notion API request error: {e}")
if e.response:
print(f"Failure response: {e.response.text}")
exit(1)
except Exception as e:
print(f"Unpredicted error occured: {e}")
exit(1)
Loading