["업다운 ESG" 개발 일기] (251125) DB구조 다시 짜기 + 앱인토스 반려사유 받아보기

■ 소개

목표: 419 (4업으로 1달러 벌기 시작해서 9달러까지 벌어보기)

https://crisp-curry-60549088.figma.site/

.

어제에 이어서 마저 관계형 데이터 베이스 구축 유튜뷰를 보고

제 나름대로 만든 다음 커서를 통해 보완받아봤습니다.

.

그리고 앱인토스에 어거지로 일단 등록 신청해보고 반려사유 받아보기를 했습니다!

.

■ 진행 및 결과

1) DB구조 다시 짜기

유튜브 보고 5개 ENTITY로 구성했는데,

커서한테 조언을 구하니 2개로 줄여줬습니다.

받은 조언은 아래와 같습니다.

ERD 및 프롬프트 개선 제안
1. MYPAGE 엔티티의 정체성 (테이블 분리 vs 통합)
현재: USER와 별도로 MYPAGE가 있고 여기에 TOTALVOTES, SUCCESSRATE 등이 있습니다.
문제: 관계형 DB에서 MYPAGE라는 테이블 명은 어색합니다. 이는 보통 USER_STATS (유저 통계) 라는 이름으로 USER와 1:1 관계를 맺거나, 트래픽이 적다면 USER 테이블에 컬럼으로 포함시킵니다.
제안: 프롬프트에 "MYPAGE는 USER_STATS 테이블로 명명하고, User 테이블과 1:1 관계를 맺는다"고 명시하는 것이 좋습니다.
2. REMAINTIME (남은 시간) 속성
현재: REMAINTIME을 INT로 저장하려고 합니다.
문제: DB에 '남은 시간(예: 60분)'을 저장하면 1분마다 모든 데이터를 업데이트해야 합니다.
제안: DEADLINE (마감 시간, DateTime)을 저장하고, 남은 시간은 쿼리 시점(NOW())에 계산하도록 변경해야 합니다.
3. RESULT와 ESG ISSUE 관계
현재: ESG ISSUE와 RESULT가 분리되어 있습니다.
분석: 하나의 이슈에 결과는 하나뿐이라면 굳이 테이블을 나눌 필요가 없습니다. (조인 비용 발생)
제안: ESG_ISSUE 테이블에 result_status, actual_change_rate 등의 컬럼을 추가하여 합치는 것이 효율적일 수 있습니다. 다만, ERD를 그대로 따르길 원하신다면 1:1 관계로 명시해야 합니다.
4. UID, IID 등 모호한 약어
제안: SQL 표준 관례에 따라 user_id, issue_id, comment_id 등으로 명확하게 작성하도록 지시하는 것이 좋습니다. 특히 Toss userKey를 사용할 것이므로 user_id는 BIGINT가 되어야 할 수도 있습니다.

.

요걸 FIREBASE용으로 다음은 MD는 다음과 같습니다.

# UPDOWNESG Firestore DB Schema

이 문서는 UPDOWNESG 프로젝트의 Firebase Cloud Firestore 데이터베이스 구조를 정의합니다.
ERD의 6개 엔티티(User, MyPage, EsgIssue, Result, Prediction, Comment)를 NoSQL 특성에 맞춰 최적화했습니다.

## 1. Collections Overview

| Collection Path | Description | ERD Mapping |
|-----------------|-------------|-------------|
| `users` | 사용자 정보 및 통계 | **USER** + **MYPAGE** |
| `users/{uid}/predictions` | 사용자의 투표/예측 기록 | **PREDICTION** |
| `issues` | ESG 이슈 정보 및 결과 | **ESG ISSUE** + **RESULT** |
| `issues/{issueId}/comments` | 이슈에 달린 댓글 | **COMMENT** |

---

## 2. Detailed Schema

### 2.1. `users` Collection
사용자의 기본 정보와 통계(MyPage) 정보를 통합하여 관리합니다.
* **Document ID**: `uid` (Firebase Auth UID)

```json
{
  // [Basic Info]
  "uid": "string (PK)",
  "email": "string",
  "nickname": "string",
  "avatarUrl": "string",
  "createdAt": "timestamp",
  "lastLoginAt": "timestamp",

  // [Stats - 구 MYPAGE]
  // 읽기 효율을 위해 별도 컬렉션이 아닌 필드로 포함
  "stats": {
    "totalVotes": "number (총 투표수)",
    "successCount": "number (예측 성공 횟수)",
    "successRate": "number (성공률 %)",
    "currentStreak": "number (현재 연속 성공)",
    "maxStreak": "number (최대 연속 성공)",
    "points": "number (보유 포인트)"
  }
}
```

### 2.2. `issues` Collection
투표 대상인 ESG 이슈와 그 결과 정보를 하나의 문서에서 생명주기(Status)로 관리합니다.
* **Document ID**: `issueId` (Auto-generated ID)

```json
{
  // [Issue Info - 구 ESG ISSUE]
  "title": "string (헤드라인)",
  "detail": "string (상세 내용)",
  "category": "string ('E' | 'S' | 'G')",
  "companyName": "string",
  "companyLogo": "string (URL)",
  
  // [Schedule]
  "publishedAt": "timestamp (게시일)",
  "deadline": "timestamp (투표 마감일)",
  
  // [Status & Votes]
  "status": "string ('voting' | 'calculating' | 'closed')",
  "voteCounts": {
    "up": "number",
    "down": "number",
    "total": "number"
  },

  // [Result - 구 RESULT]
  // status가 'closed'일 때 유효한 값
  "result": {
    "targetOutcome": "string ('UP' | 'DOWN')", // 정답
    "actualChangeRate": "number", // 실제 변동률
    "finalizedAt": "timestamp"
  }
}
```

### 2.3. `predictions` Sub-collection
* **Parent**: `users/{uid}`
* **Document ID**: `issueId` (중복 투표 방지를 위해 이슈 ID를 Key로 사용)

```json
{
  "issueId": "string (FK)",
  "vote": "string ('UP' | 'DOWN')",
  "createdAt": "timestamp",
  
  // 결과 처리 후 업데이트되는 필드
  "status": "string ('waiting' | 'success' | 'fail')",
  "earnedPoints": "number",

  // [Snapshot]
  // 목록 조회 시 Join 없이 UI를 그리기 위한 중복 데이터
  "snapshot": {
    "issueTitle": "string",
    "companyName": "string",
    "category": "string",
    "deadline": "timestamp"
  }
}
```

### 2.4. `comments` Sub-collection
* **Parent**: `issues/{issueId}`
* **Document ID**: `commentId` (Auto-generated ID)

```json
{
  "content": "string",
  "createdAt": "timestamp",
  "likeCount": "number",

  // [Writer Snapshot]
  // 작성자 정보를 매번 join하지 않기 위함
  "writer": {
    "uid": "string",
    "nickname": "string",
    "avatarUrl": "string",
    "vote": "string ('UP' | 'DOWN' | null)" // 작성 당시 투표 포지션
  }
}
```

## 3. Indexing Strategy (참고)
* **기본 인덱스**: Firestore가 단일 필드에 대해 자동 생성.
* **복합 인덱스(Composite Index)** 필요 예상:
  * `issues`: `status` + `deadline` (마감 임박순 정렬)
  * `predictions`: `uid` + `createdAt` (내 활동 타임라인)
  * `comments`: `issueId` + `createdAt` (댓글 최신순 정렬)

.

실제로 FIREBASE DB를 업데이트 해달라고 했고,

그 결과는 다음과 같습니다.

Google 스프레드시트의 스크린샷
데이터 초기화가 끝나면 Firebase 콘솔을 새로고침해서 다음을 확인해보세요:
daily_logs 컬렉션이 사라졌는지 (이제 사용하지 않음)
루트 레벨의 predictions 컬렉션이 사라졌는지 (이제 users 하위로 이동)
users와 issues 컬렉션만 깔끔하게 남아있는지

.

정확히 뭔지는 모르지만,

네트워크 방에서 정혜지 님께 배운데로

ERD 를 yaml 형식으로 바꿔서 https://dbdiagram.io/d에서 돌려본 결과물은 아래와 같습니다.

항목 목록을 보여주는 웹 브라우저의 스크린샷

.

2) 앱인토스 반려사유 받아보기

며칠 전에 무작정 일단 앱을 등록해봤습니다.

반려사유를 준다고 하는데, 어제 밤에는 업무시간이 끝나서 회신 받지 못했습니다.

그래서 오늘 업무시간에 다시 여쭤봤습니다.

.

뭔가 사유를 말씀주실 줄 알았는데,

의외로

"구체적인 서비스 내용 및 목적"이 뭐냐고 물어보셨습니다.

다시 내부 확인 후 안내주신다고 합니다.

한국어로 된 문자 메시지의 스크린샷

.

친절한 Q&A 시스템이 갖춰져있는 것 같아 마음이 든든합니다.

뭔가 막혀도 물어볼 구석이 있다는게 좋습니다.

.

3) 여담

사업자등록했습니다 ㅎㅎ 그냥 하루만에 뚝닥했고, 토스 로그인 등을 달기 위해 대표관리자로 신청해두었습니다

■ 느낀점 및 향후 계획

이왕 사업자등록까지 한김에 조금 더 고삐를 죄어서

출시까지는 안되더라도 계속해서 앱인토스 검토요청을 넣어볼 생각입니다.

1달러 벌어보자 ㅎㅎ

2
3개의 답글

뉴스레터 무료 구독

👉 이 게시글도 읽어보세요