백로그
0할 일
4성능 P1: 복합 인덱스 6개 추가
마이그레이션으로 복합 인덱스 추가: 1. payments [status, approved_at] 2. payments [status, created_at] 3. blog_posts [status, published_at] 4. diagnosis_sessions [status, end_time] 5. diagnosis_sessions [user_id, status, created_at] 6. workspace_engagement_surveys [workspace_id, status]
성능 P2: Admin 대시보드/통계 캐싱 적용
admin/dashboard 8개 COUNT, admin/statistics 6+ 집계 쿼리에 Rails.cache.fetch 적용 (5분 TTL). - DiagnosisTarget.find_by(slug:) 등 정적 데이터도 캐싱
성능 P2: counter_cache 도입 (diagnosis_sessions_count)
users 테이블에 diagnosis_sessions_count 컬럼 추가. - DiagnosisSession에 counter_cache: true 설정 - Admin 사용자 목록에서 .count 쿼리 제거
커리어 캔버스 워크숍(어드민 회차) 보완 — 보류, 다시 쓸 때 먼저 고칠 3가지
2026-09-30 대표님 결정: 일단 안 쓰고 나중에 고친다. 회차를 만들지 않으면 아무 영향 없음(현재 0개). 설명 페이지(화면 캡처 예시): https://claude.ai/artifact/UnvFHMNn59P9DTsNseSyce 다시 쓸 때 먼저 고칠 것 1. 끝 시각이 지나면 회차 표시(회차 이름 · 시각 · 조 이야기)와 「마감 시각이 지났어요」 배너를 내린다. 지금은 한 번 묶이면 영구히 남는다(기간 잠금만 남길지 결정). 2. 회차 시간에 새로 연 캔버스만 묶는다. 지금은 그룹 사람이 교육 전에 쓰던 개인 캔버스도 회차 시간에 열면 묶여, 목표 기간이 회차 설정으로 바뀌고 10년 칸이 화면에서 사라진다(CareerCanvas#attach_workshop!). 3. 고객사 HR(워크스페이스 관리자)용 이름 없는 진행 판. 지금은 9WAY 시스템 어드민에서만 보인다. 전제: 참가자마다 강점 진단(직장인 · 성인 · 리더) + BASIC 이상(워크스페이스 크레딧 1)이 열려 있어야 캔버스를 쓸 수 있다. 한국어 화면만.
진행 중
1서비스 가이드(/services) 점검과 손보기: 사진 순서 · 옛 문구 · 숨김 · 리포트 · 몰입 · 성과 관리 가이드
2026-10-14 대표님 요청. 운영 서비스 가이드 26편 점검표: https://claude.ai/artifact/C2YTonmYAASbUA4GdG5DJm 진행 순서(대표님 결정) 1. 사진 자리 어긋남(25편) 고침 배포 — 완료 2. 행복도 2편 · 커리어테크 5편 숨김(게시만 끔) — 완료 3. 화면 문구 4건 고침(Big5 안내 · 심화 진단 부제 · 결과 화면 행복도 카드 · 대량 이메일 초대 영어) — 완료 4. 「강점 리포트」 가이드 새로 쓰기 → 조직 진단(몰입) 가이드 고치기 → 「성과 관리」 가이드 새로 쓰기 스킬: .claude/skills/feature-guide
리뷰
19[조직문화 PR1] DB: WorkspaceEngagementSurvey 모델 + DiagnosisSession 확장
## 설명 워크스페이스별 분기 설문 관리 테이블 생성 및 DiagnosisSession에 B2B 연결 FK 추가. ### 새 모델: WorkspaceEngagementSurvey - workspace_id: uuid, NOT NULL, FK(workspaces), index - created_by_id: uuid, NOT NULL, FK(users) - title: string, NOT NULL (예: "2026 Q1") - year: integer, NOT NULL - quarter: integer, NOT NULL (1-4) - status: integer, NOT NULL, default: 0 (draft/active/closed) - access_token: string, NOT NULL, unique index - started_at, ended_at: datetime - 복합 인덱스: [workspace_id, year, quarter] unique ### DiagnosisSession 확장 - workspace_engagement_survey_id: uuid, FK(workspace_engagement_surveys), index - workspace_group_id: uuid, FK(workspace_groups), index - 기존 user_id(optional) 활용: 로그인 사용자는 참고용으로 기록, 비로그인도 허용 - context_type = "b2b"로 설정 ### 설계 결정 - Option A 채택: DiagnosisSession 재사용 (기존 EngagementCalculator/ResultFormatter 완전 재사용) - 익명성: 결과 집계에서 user_id 사용하지 않음. 3명 이하 그룹 결과 비공개 ## 완료 기준 - [ ] 마이그레이션 실행/롤백/재실행 성공 - [ ] WorkspaceEngagementSurvey 모델: 유효성, 연관관계, 스코프 테스트 통과 - [ ] DiagnosisSession 연관관계 추가 테스트 통과 - [ ] fixture 생성 - [ ] engagement_b2b DiagnosisType seed 확인 (없으면 생성) ## 의존성 없음
[조직문화 PR2] 서비스: 설문 생성/응답 제출/결과 집계
## 설명 3개 서비스 객체 구현. ### EngagementSurveys::CreateService - 워크스페이스 + 연도/분기로 설문 생성 - access_token 자동 발급 (SecureRandom.alphanumeric(8).upcase) - 동일 워크스페이스+분기 중복 방지 ### EngagementSurveys::SubmitResponseService - 그룹 선택 + 10문항 응답을 DiagnosisSession으로 저장 - context_type = "b2b", workspace_engagement_survey_id, workspace_group_id 설정 - 로그인 사용자는 user_id 참고용 기록, 비로그인도 허용 - 기존 EngagementCalculator로 채점 ### EngagementSurveys::ResultAggregator - 전체/그룹별 집계 (3명 이하 그룹 결과 nil 반환) - overall 점수 = 10번 문항 전체 평균 - 몰입 유형 비율 = 10번 문항 기준: 적극적(6,7) / 소극적(4,5) / 비몰입(1,2,3) - 3요인 분석: psychological_safety, self_efficacy, flow_experience 평균 - 9요인 분석: 9개 dimension별 평균 - 강약점 해석 포함 ## 완료 기준 - [ ] 각 서비스 단위 테스트 통과 - [ ] 3명 이하 그룹 비공개 로직 테스트 - [ ] 몰입 유형 비율 계산 정확성 테스트 - [ ] 기존 EngagementCalculator와 정상 연동 확인 ## 의존성 PR1 완료 후
[조직문화 PR3] 컨트롤러: 워크스페이스 설문 관리 (CRUD + 결과)
## 설명 Workspaces::EngagementSurveysController 구현 (인증 필요, admin/leader 권한). ### 액션 - index: 분기 리스트 (연도/분기, 상태, 응답 수) - new/create: 연도+분기 선택하여 설문 생성 → QR코드+링크 자동 생성 - show: 결과 대시보드 (전체/그룹별) - toggle_status: draft→active→closed 전환 ### 라우트 ```ruby resources :workspaces do resources :engagement_surveys, only: [:index, :new, :create, :show], controller: "workspaces/engagement_surveys" do member { patch :toggle_status } end end ``` ### 권한 매트릭스 | 액션 | member | leader | admin | |------|--------|--------|-------| | index | - | O | O | | create | - | - | O | | show(결과) | - | O | O | | toggle_status | - | - | O | ## 완료 기준 - [ ] 컨트롤러 테스트 통과 (각 액션 + 권한) - [ ] 라우트 정상 동작 - [ ] WorkspaceAuthorization concern 활용 ## 의존성 PR1, PR2 완료 후
[조직문화 PR4] 공개 응답 플로우: 비인증 설문 응답 컨트롤러
## 설명 EngagementSurveyResponsesController 구현 (인증 불필요, access_token 기반). ### 라우트 - GET `/engage/:access_token` → 그룹 선택 화면 - POST `/engage/:access_token/start` → 안내 확인 후 문항 시작 - POST `/engage/:access_token/submit` → 10문항 응답 제출 - GET `/engage/:access_token/complete` → 완료 화면 ### 플로우 1. QR/링크로 접속 → 해당 워크스페이스 그룹 목록 표시 2. 그룹 선택 시 안내: - "약 5분 소요" - "3명 이하 그룹은 결과가 측정되지 않습니다" - "응답자 정보는 기록되지 않습니다" (심리적 안정감) 3. 10문항 7점 리커트 척도 응답 4. 완료 화면 (감사 메시지) ### 특이사항 - allow_unauthenticated_access 적용 - 로그인 사용자는 user_id를 참고용으로 기록 (Current.user 체크) - 설문 status가 active일 때만 응답 가능 - 기존 _engagement_question.html.erb 패턴 참고 ## 완료 기준 - [ ] 비인증 접근 테스트 통과 - [ ] 그룹 선택 → 문항 응답 → 완료 플로우 테스트 - [ ] active 상태가 아닌 설문 접근 차단 테스트 - [ ] 로그인/비로그인 모두 응답 가능 확인 ## 의존성 PR1, PR2 완료 후
[일괄다운로드 PR1] BulkReportDownload 모델 + 마이그레이션
## 설명 일괄 리포트 다운로드 비동기 처리를 위한 DB 레이어. ### 모델: BulkReportDownload - `workspace_id` (references, not null, index) - `user_id` (references, not null) — 요청한 관리자 - `status` (string, default: "pending") — pending/processing/completed/failed - `member_count` (integer) — 선택된 멤버 수 - `processed_count` (integer, default: 0) — 진행 상황 - `error_message` (text, nullable) - `member_ids` (text[]) — 선택된 workspace_member IDs - `has_one_attached :file` (ZIP) ### TDD 1. validation: workspace, user 필수 2. status enum: pending → processing → completed/failed 3. scope: expired (created_at < 7.days.ago) 4. association: belongs_to workspace/user, has_one_attached file ### 참고 패턴 - Active Storage: `Coaching::Recording`, `Study::Assignment` 등 4개 모델에서 사용 중 ## 완료 기준 - [ ] 마이그레이션 정상 실행 - [ ] 모델 validation, scope, enum 테스트 통과 - [ ] fixture 생성
[크레딧통합 PR1] DB 마이그레이션 — balance decimal 변환 + 데이터 이관
## 설명 개인 크레딧(UserCredit)의 balance를 integer → decimal(10,1)로 변환하고, diagnosis_balance 잔액을 balance로 이관합니다. ### 마이그레이션 내용 1. `user_credits.balance`: integer → decimal(10,1), default: 0.0 2. `credit_transactions.amount`: integer → decimal(10,1) 3. `credit_transactions.balance_after`: integer → decimal(10,1) 4. 데이터 이관: `UPDATE user_credits SET balance = balance + (diagnosis_balance * 0.2)` 5. `UserCredit`에 `self.ignored_columns += ["diagnosis_balance"]` 추가 (2단계 제거 패턴) ### 주의사항 - migrate/rollback/migrate 3단계 검증 필수 - development DB 카운트 변동 없음 확인 - 기존 구매자 없으므로 이관 리스크 낮음 ## 완료 기준 - [ ] 마이그레이션 migrate/rollback/migrate 통과 - [ ] `balance` 컬럼이 decimal(10,1) 타입 - [ ] `credit_transactions.amount/balance_after`가 decimal(10,1) - [ ] 기존 데이터 이관 완료 (balance에 diagnosis_balance * 0.2 합산) - [ ] `ignored_columns`에 diagnosis_balance 추가 - [ ] development DB 카운트 변동 없음 ## 의존성 없음 (첫 번째 작업) ## 관련 파일 - `db/migrate/YYYYMMDD_unify_credit_balance.rb` (신규) - `app/models/user_credit.rb`
[리드관리 PR1] 리드 목록 + 퍼널 스코프
계획서: docs/features/admin_lead_management.md (§3, §6, §7-PR1) 목표: 운영자가 퍼널별(가입자/무료진단/유료진단/과정신청/수료자) 리드를 어드민에서 바로 조회. 읽기 전용, 위험 낮음. MVP 첫 단계. 작업: - User 퍼널 스코프 추가(app/models/user.rb): with_completed_diagnosis, with_paid_diagnosis, free_diagnosis_only, course_graduates, marketing_subscribed, not_suppressed(※PR3 전까지는 EmailSuppression 미존재 → 이 스코프는 PR3에서 추가). User#funnel_stage(도달 최상위 단계 심볼) 구현. - 인덱스 마이그레이션: diagnosis_sessions(is_report_purchased), diagnosis_sessions(user_id), expert_certificates(user_id) 확인/추가. - Admin::Marketing::LeadsController#index — 기존 admin/users#index 패턴(검색/필터/정렬/_pagination) 복제. 컬럼: 이름/이메일, 퍼널 단계 배지, 진단 수, 마지막 활동, 수신동의. 필터: 퍼널 단계, 타겟, 수신동의, 가입/진단 기간, 검색(이름/이메일/전화). #show는 기존 admin_user_path로 링크. - 라우트: namespace admin > namespace marketing > resources :leads, only: [index, show]. - 사이드바(_sidebar.html.erb) "리드 관리" 그룹 + nav_item 3개(leads/segments/campaigns) 추가 + ko.yml admin.nav 라벨 추가. (en/zh/vi 금지) 테스트(Minitest+fixtures): - free_diagnosis_only: completed 보유 & 유료 0만 포함, 유료 보유자 제외. - with_paid_diagnosis: is_report_purchased=true 세션 보유자만. - course_graduates: completion 인증서 보유자만. - funnel_stage: 수료자는 유료/무료 보유와 무관하게 graduate 반환. - LeadsController#index: 비관리자 차단, 퍼널 필터 동작, 페이지네이션, ko 강제. 완료 기준: 어드민 사이드바 "리드 관리 > 리드"에서 퍼널별 필터·검색·페이지네이션 동작.
[일괄다운로드 PR2] BulkReportGenerateJob + CleanupJob
## 설명 백그라운드 PDF→ZIP 생성 Job + 7일 만료 삭제 Job. ### BulkReportGenerateJob - BulkReportDownload를 find → status "processing" - member_ids로 멤버 + completed/purchased 세션 조회 - 멤버별 순차 PDF 생성 (Diagnosis::PdfGenerator 재사용) - Tempfile ZIP → Active Storage attach - processed_count 갱신 (진행률 추적) - 완료 시 status "completed" + BulkReportMailer 발송 - 실패 시 개별 skip, 전부 실패면 status "failed" - 메모리 관리: 10건마다 GC.start ### BulkReportCleanupJob - 7일 경과 BulkReportDownload 레코드 + 첨부파일 삭제 - config/recurring.yml에 매일 2am 등록 ### TDD - Generate: 정상(completed), 부분실패(skip), 전체실패(failed), processed_count - Cleanup: 7일 경과 삭제, 7일 이내 유지 ### 참고 패턴 - `AiInsightGenerateJob`: status 추적 패턴 - `CleanupPageViewsJob`: recurring 패턴 ## 의존성 PR1 완료 후 ## 완료 기준 - [ ] Generate job: PDF→ZIP→attach 플로우 테스트 통과 - [ ] Cleanup job: 만료 레코드 삭제 테스트 통과 - [ ] recurring.yml에 cleanup 등록
[크레딧통합 PR2] UserCredit + CreditTransaction 모델 통합
## 설명 UserCredit 모델에서 diagnosis_balance 관련 메서드를 모두 제거하고 단일 크레딧 체계로 통합합니다. ### 변경 내용 - `UserCredit`: diagnosis_sufficient?, deduct_diagnosis!, add_diagnosis! 제거 - `UserCredit`: sufficient?, deduct!, add! — decimal 호환으로 변경, credit_type "unified" 사용 - `CreditTransaction`: credit_type 유효값에 "unified" 추가 - `DiagnosisType`: `requires_diagnosis_credit?` → `credit_cost` 메서드로 변경 - strength 카테고리: nil (리포트 구매로 별도 처리) - big5/happiness/growth_mindset: 0.2 ### TDD 순서 1. RED: 기존 테스트를 통합 크레딧 기준으로 수정 + 0.2 단위 차감 테스트 2. GREEN: 모델 수정 3. REFACTOR: 불필요 메서드 정리 ## 완료 기준 - [ ] `credit.deduct!(0.2, ...)` 정상 동작 - [ ] `credit.sufficient?(0.2)` 정상 동작 - [ ] diagnosis 관련 메서드 제거됨 (NoMethodError) - [ ] CreditTransaction credit_type "unified" 지원 - [ ] DiagnosisType#credit_cost 동작 - [ ] 모델 테스트 100% 통과 ## 의존성 PR1 (DB 마이그레이션) 완료 후 ## 관련 파일 - `app/models/user_credit.rb` - `app/models/credit_transaction.rb` - `app/models/diagnosis_type.rb` - `test/models/user_credit_test.rb` - `test/fixtures/user_credits.yml`
[리드관리 PR2] 세그먼트 (조건 저장형 동적 세그먼트)
계획서: docs/features/admin_lead_management.md (§4.1, §4.5, §6, §7-PR2) 선행: PR1. 목표: 운영자가 필터 조건을 저장하면 대상이 자동 산출되는 동적 세그먼트. 캠페인 발송 대상의 기반. 작업: - 마이그레이션 marketing_segments(uuid): name(unique), description, conditions(jsonb), cached_count(int,0), count_refreshed_at, created_by_id(FK users). - MarketingSegment 모델 + 검증. - Marketing::SegmentQuery 서비스: conditions(jsonb) → AR 스코프 변환. 화이트리스트 기반(허용 키/값 외 무시 → SQL 인젝션 차단). 지원 조건(MVP): funnel_stages(OR), facts(AND: has_paid_diagnosis 등), diagnosis_target, locale, email_subscription, created_after, first_diagnosis_after, days_since_diagnosis_gte, utm_source. - Admin::Marketing::SegmentsController CRUD + member post :refresh_count. 조건 빌더 폼(select/checkbox). show: 대상 수 + 샘플 리드 20명 + "이 세그먼트로 캠페인 만들기" 버튼. 저장/refresh 시 cached_count 갱신. 테스트: - SegmentQuery: 미허용 키/값 무시(인젝션 차단), funnel_stages OR, facts AND 결합. - days_since_diagnosis_gte 경계값 정확. - refresh_count가 실제 대상 수와 일치. 주의: "진단 결과 유형(WAY/DOMAIN)" 조건은 §4.5 스파이크(결과 점수 저장 위치 확인) 후 fast-follow. 이 PR 범위에서 제외. 완료 기준: 세그먼트 생성/저장 후 show에서 자동 산출된 대상 수·샘플 노출.
성과 관리(OKRT) 1단계 — 뼈대 구현
구현안 docs/plans/leader-duties-workspace/index.html 의 10절 1단계. - okr_* 데이터 모델 (cycles, objectives, key_results, owners, alignments, milestones, tasks, task_assignees, checkins, change_logs) - 메뉴·권한: performance 키, MENU_FEATURES, 라벨 5개 언어, 사이드바 - 빈 첫 화면 · 정렬 만들기 · 이번 주(O→KR→TASK + 팀원 목록) · KR 상세 · 주간 점검 · 월요일 알림 - 용어 O·KR·TASK, 상태 정상·주의·위험·달성, KR 책임자 1명 권장(여럿 허용), TASK 담당 여럿
[일괄다운로드 PR3] BulkReportMailer (완료/실패 이메일)
## 설명 ZIP 생성 완료/실패 시 요청한 관리자에게 이메일 발송. ### BulkReportMailer - `completed(bulk_report_download:)`: 워크스페이스명, 멤버수, 다운로드 링크 포함 - `failed(bulk_report_download:)`: 에러 메시지 포함 ### 뷰 - `app/views/bulk_report_mailer/completed.html.erb` - `app/views/bulk_report_mailer/failed.html.erb` ### i18n - `bulk_report_mailer.completed.subject`: "워크스페이스 리포트가 준비되었습니다" - `bulk_report_mailer.failed.subject`: "리포트 생성에 실패했습니다" - 4개 언어 (ko, en, zh, vi) ### 참고 패턴 - GiftMailer: keyword args, t(".subject"), ApplicationMailer 상속 ## 의존성 PR1 완료 후 (PR2와 병렬 가능) ## 완료 기준 - [ ] completed 메일 발송 테스트 통과 - [ ] failed 메일 발송 테스트 통과 - [ ] 4개 언어 i18n 키 추가
[크레딧통합 PR3] 서비스 객체 통합 (구매/결제/선물/추천)
## 설명 크레딧 관련 서비스 객체를 단일 크레딧 체계에 맞게 통합합니다. ### 변경 내용 1. **DiagnosisCreditPurchaseService** — 삭제 2. **ConfirmationService** — diagnosis_credit 결제 타입 분기 제거 3. **CancellationService** — revoke_diagnosis_credits!, validate_diagnosis_credit_unused! 제거 4. **Referrals::RewardService** — `add_diagnosis!(1)` → `add!(0.2)` 변경 5. **GiftService** — GIFT_TYPES 재설계: - strength_diagnosis + advanced_diagnosis → "credit" (cost: 1.0, 개인에게 1크레딧 선물) - career_points 유지 (cost: 0.5 → 20P) - grant_strength_credits! + grant_diagnosis_credits! → grant_credits! 통합 - 자기 선물 허용 (워크스페이스 크레딧 → 본인 개인 크레딧) ### TDD 순서 1. RED: 서비스 테스트 작성 (통합 크레딧 기준) 2. GREEN: 서비스 수정/삭제 3. REFACTOR ## 완료 기준 - [ ] DiagnosisCreditPurchaseService 삭제됨 - [ ] ConfirmationService에서 diagnosis_credit 분기 없음 - [ ] CancellationService에서 diagnosis_credit 분기 없음 - [ ] 추천 보상이 0.2 크레딧 지급 - [ ] GiftService "credit" 선물 타입 동작 (1크레딧) - [ ] 자기 선물 가능 (admin == workspace_member.user) - [ ] career_points 선물 기존대로 동작 - [ ] 서비스 테스트 100% 통과 ## 의존성 PR2 (모델 통합) 완료 후 ## 관련 파일 - `app/services/payments/diagnosis_credit_purchase_service.rb` — 삭제 - `app/services/payments/confirmation_service.rb` - `app/services/payments/cancellation_service.rb` - `app/services/referrals/reward_service.rb` - `app/services/workspaces/gift_service.rb` - `test/services/` 관련 테스트
[리드관리 PR3] Resend 발송 기반 (웹훅·수신거부·suppression)
계획서: docs/features/admin_lead_management.md (§5, §7-PR3) 목표: 마케팅 발송 인프라. 발송 자체(PR4) 전에 로그/웹훅/컴플라이언스 토대를 만든다. 기존 트랜잭션 메일(Gmail SMTP)은 건드리지 않음. 운영 현황: 9way.co.kr Resend 인증 완료(Tokyo). RESEND_API_KEY 설정 완료. 이 PR에서 RESEND_WEBHOOK_SECRET + MARKETING_FROM_EMAIL(marketing@9way.co.kr) 추가 필요(.env / .kamal/secrets / .env.deploy). 작업: - Gemfile gem "resend" + config/initializers/resend.rb (Resend.api_key = ENV["RESEND_API_KEY"]). delivery_method 전역 변경 금지. - 마이그레이션: email_deliveries(campaign_id, user_id, email_address, resend_email_id[unique idx], status, delivered_at/opened_at/clicked_at/bounced_at, open_count/click_count, error_message; idx [campaign_id,status], user_id), email_suppressions(email_address[unique], reason enum, suppressed_at). - EmailDelivery, EmailSuppression 모델. - User 스코프 not_suppressed 추가(PR1에서 보류분). - Webhooks::ResendController#create — POST /webhooks/resend, Svix 서명검증(RESEND_WEBHOOK_SECRET), 멱등. 이벤트→deliveries/카운터: delivered/opened(고유+open_count)/clicked/bounced(→suppression hard_bounce)/complained(→suppression complaint + user.email_subscription=false). - EmailSubscriptionsController#unsubscribe — GET /email/unsubscribe?token=, message_verifier(:unsubscribe)로 user id 서명(신규 컬럼 불필요). 처리: email_subscription=false + EmailSuppression(unsubscribe). 확인 페이지(ko). - 브랜드 이메일 레이아웃 partial + unsubscribe 링크 + List-Unsubscribe/List-Unsubscribe-Post 헤더. - Marketing::TokenRenderer — 화이트리스트 토큰만 치환({{name}},{{funnel_stage}} 우선; {{primary_way}}/{{primary_domain}}는 스파이크 후), HTML 이스케이프. 테스트: - 웹훅 서명 실패 401 / 성공 200. - 각 이벤트가 delivery·카운터 갱신, 같은 이벤트 재수신 멱등. - complained → email_subscription=false + suppression. - unsubscribe 유효 토큰 해제 / 위조 토큰 거부. - TokenRenderer 화이트리스트·이스케이프. 완료 기준: 웹훅/unsubscribe/suppression 동작(실제 발송은 PR4). 외부 호출은 테스트에서 stub.
[일괄다운로드 PR4] Controller + Routes 비동기 전환
## 설명 handle_bulk_download를 비동기로 전환하고 상태 조회/파일 다운로드 엔드포인트 추가. ### Controller 변경 (workspaces/members_controller.rb) - `handle_bulk_download`: BulkReportDownload.create! + Job enqueue + redirect with notice - 중복 방지: 같은 workspace에 pending/processing 있으면 거부 - 새 액션 `bulk_download_status`: JSON (status, processed_count, member_count, download_url) - 새 액션 `bulk_download_file`: Active Storage redirect (workspace 소속 + 요청자 확인) ### Routes 추가 ```ruby collection do get :bulk_download_status get :bulk_download_file end ``` ### TDD - handle_bulk_download: 레코드 생성 + Job enqueue + redirect - bulk_download_status: 각 status별 JSON 응답 - bulk_download_file: completed면 redirect, 아니면 404 - 인가: admin/leader만 ### 참고 패턴 - `diagnoses#ai_analysis_status`: JSON 폴링 엔드포인트 ## 의존성 PR1, PR2 완료 후 ## 완료 기준 - [ ] 비동기 요청 → 폴링 → 다운로드 플로우 테스트 통과 - [ ] 중복 요청 거부 테스트 통과 - [ ] 인가 테스트 통과
[크레딧통합 PR4] 컨트롤러 + 라우트 통합
## 설명 크레딧 관련 컨트롤러와 라우트를 단일 크레딧 체계에 맞게 통합합니다. ### 변경 내용 1. **DiagnosisCreditsController** — 삭제 2. **config/routes.rb** — `resources :diagnosis_credits` 제거 3. **DiagnosesController** (L404-431): - `requires_diagnosis_credit?` → `credit_cost` 사용 - `diagnosis_sufficient?` → `sufficient?(0.2)` - `deduct_diagnosis!(1)` → `deduct!(0.2)` 4. **Admin::UsersController** (L110-142): - credit_type 분기 제거, 단일 크레딧으로 처리 5. **PaymentsController** (L164-166): - diagnosis_credit redirect 분기 제거 ### TDD 순서 1. RED: 컨트롤러 테스트 작성 (통합 크레딧 기준) 2. GREEN: 컨트롤러 수정 3. REFACTOR ## 완료 기준 - [ ] DiagnosisCreditsController 삭제됨 - [ ] 라우트에서 diagnosis_credits 제거됨 - [ ] 심화 진단 시작 시 0.2 크레딧 차감 - [ ] 관리자 크레딧 지급/차감이 단일 타입 - [ ] PaymentsController redirect 정상 동작 - [ ] 컨트롤러 테스트 100% 통과 ## 의존성 PR3 (서비스 통합) 완료 후 ## 관련 파일 - `app/controllers/diagnosis_credits_controller.rb` — 삭제 - `app/controllers/diagnoses_controller.rb` - `app/controllers/admin/users_controller.rb` - `app/controllers/payments_controller.rb` - `config/routes.rb` - `test/controllers/` 관련 테스트
[리드관리 PR4] 캠페인 작성 + 수동 발송 (Resend 배치)
계획서: docs/features/admin_lead_management.md (§4.2, §5.2, §6, §7-PR4) 선행: PR2(세그먼트), PR3(발송 기반). MVP 마지막 단계. 목표: 운영자가 세그먼트를 골라 메일을 작성·미리보기·테스트 후 Resend로 발송하고 성과를 확인. 작업: - 마이그레이션 email_campaigns(uuid): name, subject, preheader, body, from_name(default "9WAY"), reply_to, segment_id(FK), status enum(draft/scheduled/sending/sent/failed), scheduled_at(컬럼만), sent_at, recipients_count + delivered/opened/clicked/bounced/unsubscribed_count, created_by_id. - EmailCampaign 모델 + 상태 전이. - Admin::Marketing::CampaignsController CRUD(index/new/create/show) + member post :send_test, post :deliver. 작성 폼(제목/본문 + 토큰 도우미 + 미리보기), 테스트 발송(본인), 발송 트리거. show: status + 성과(발송/도달/오픈/클릭/바운스/수신거부) + deliveries 테이블. - Marketing::CampaignSender 서비스: 세그먼트 대상 산출 → 게이트 필터(marketing_subscribed & not_suppressed & email 존재) → 수신자별 토큰 렌더 + 레이아웃 래핑 + unsubscribe 주입 → 100명 단위 Resend::Batch.send → 반환 id로 email_deliveries 생성(status=sent). Resend 클라이언트는 주입 가능하게 분리(테스트 mock). - Marketing::SendCampaignJob (Solid Queue) 비동기. 배치 일부 실패는 delivery=failed + error_message, 캠페인 부분성공 허용. 테스트: - CampaignSender 게이트: 수신거부·suppression·이메일 없음 제외. - 100명 초과 시 배치 분할. - 발송 후 deliveries 생성 + status=sent/recipients_count 일치. - send_test는 본인에게만. - 배치 일부 실패 시 해당 delivery=failed, 캠페인 부분성공. - Resend 호출 stub(테스트에서 실제 발송 금지). 완료 기준: 어드민에서 세그먼트 선택→작성→테스트→발송, show에서 웹훅 기반 성과 갱신 확인. = MVP 완성.
[일괄다운로드 PR5] View + Stimulus 폴링 배너 UI
## 설명 멤버 목록 페이지에 다운로드 상태 배너 + Stimulus 폴링 컨트롤러. ### Stimulus Controller: bulk_download_status_controller.js - connect: status endpoint 폴링 (3초 간격) - pending/processing: 프로그레스 배너 (processed_count / member_count) - completed: 다운로드 링크 배너 + 폴링 중지 - failed: 에러 배너 + 폴링 중지 - 활성 다운로드 없으면 배너 숨김 + 폴링 중지 ### Partial: _bulk_download_banner.html.erb - index.html.erb의 `<div id="members-list">` 바로 아래 삽입 - data-controller="bulk-download-status" - data-bulk-download-status-url-value로 폴링 URL 전달 ### i18n (4개 언어) - workspaces.members.bulk_download.pending/processing/completed/failed - workspaces.members.bulk_download.download_ready - workspaces.members.flash.bulk_download_started ## 의존성 PR4 완료 후 ## 완료 기준 - [ ] 배너 렌더링 + 폴링 동작 - [ ] 상태별 UI 전환 (pending → processing → completed/failed) - [ ] 다운로드 링크 클릭 시 ZIP 다운로드 - [ ] i18n 4개 언어
[크레딧통합 PR5] 뷰/UI 통합 + i18n + diagnosis_balance 컬럼 제거
## 설명 모든 뷰에서 진단 크레딧 관련 UI를 제거하고 단일 크레딧 표시로 통합합니다. diagnosis_balance 컬럼도 최종 제거합니다. ### 변경 내용 (뷰 11개) 1. `diagnosis_credits/new.html.erb` — 삭제 2. `credits/index.html.erb` — 2열 그리드 → 단일 잔액 카드, credit_type 뱃지 단순화 3. `diagnoses/index.html.erb` — diagnosis_credit 관련 표시/링크 통합 4. `diagnoses/_diagnosis_type_card.html.erb` — 크레딧 안내 변경 5. `diagnoses/_insufficient_credits.html.erb` — 메시지 통합 ("0.2 크레딧 필요") 6. `admin/users/show.html.erb` — 진단/리포트 분리 표시 → 단일 크레딧 7. `shared/_sidebar.html.erb` — "NR"+"D" 2개 뱃지 → 단일 뱃지 8. `workspaces/members/_gift_modal.html.erb` — 3종 → "credit"(1크레딧) + career_points 9. `report_credits/new.html.erb` — "크레딧 구매"로 텍스트 통합 ### 기타 - `diagnosis_credit_purchase_controller.js` — 삭제 - `credits_helper.rb` — credit_type 관련 메서드 단순화 - i18n 4개 언어: diagnosis_credits 키 제거, 통합 메시지 - `db/migrate/YYYYMMDD_remove_diagnosis_balance.rb` — 컬럼 최종 제거 - `UserCredit`에서 `ignored_columns` 제거 ## 완료 기준 - [ ] 모든 뷰에서 단일 크레딧 잔액만 표시 - [ ] diagnosis_credits 뷰/JS 삭제됨 - [ ] 사이드바 단일 뱃지 표시 - [ ] 선물 모달 "credit" + "career_points" 2종만 표시 - [ ] i18n 4개 언어 정리 완료 - [ ] diagnosis_balance 컬럼 DB에서 제거됨 - [ ] bin/ci 통과 ## 의존성 PR4 (컨트롤러 통합) 완료 후 ## 관련 파일 - `app/views/diagnosis_credits/` — 삭제 - `app/views/credits/index.html.erb` - `app/views/diagnoses/index.html.erb`, `_diagnosis_type_card.html.erb`, `_insufficient_credits.html.erb` - `app/views/admin/users/show.html.erb` - `app/views/shared/_sidebar.html.erb` - `app/views/workspaces/members/_gift_modal.html.erb` - `app/javascript/controllers/diagnosis_credit_purchase_controller.js` — 삭제 - `app/helpers/credits_helper.rb` - `config/locales/{ko,en,zh,vi}.yml`, `diagnosis/*.yml`
완료 (7일)
6워크스페이스 초대에 언어 선택 추가
링크·이메일·대량 이메일 초대 폼에 언어 칸(기본 한국어)을 넣고, 초대 링크·메일·결과 메일 링크가 고른 언어를 따르게 한다. 남은 일: 결과 메일 본문 번역, 커밋·배포.
세그먼트 날짜 필터가 오전 9시를 경계로 자르는 문제 수정
마케팅 세그먼트의 날짜 필터가 한국 날짜 0시 대신 오전 9시(UTC 0시)를 경계로 삼아, 고른 날짜 0~9시 대상자가 빠진다.
폰에서 「리포트 제공」 상품 칸이 화면 밖으로 넘치는 문제 수정
초대 폼의 상품 선택 칸 너비가 가장 긴 FULL 상품 이름에 맞춰 428px까지 늘어나, 390px 폰에서 화면이 옆으로 밀린다. 칸을 카드 폭 안에 맞춘다.
브랜딩 문장이 고른 언어로 나오게 고치고 취약 젬 올리기
영어 화면에서 My Strength Branding 문장이 한국어로 나왔다. 지시문이 한국어로 고정돼 있었다. 언어를 직접 지정하고 규칙을 한국어에 맞췄다. 위저드 칸과 K PDF 표지가 긴 문장을 담게 고쳤다. bundler-audit 46건을 올려 0건으로 만들었다.
다른 언어 화면에서 「강점 개발」 탭과 「강의실」 메뉴 숨기기
수강 권한이 있는 계정으로 en·ja·zh·vi 결과 화면을 열면 번역 안 된 「강점 개발」 탭이 보이고, 사이드바에 My Courses가 보인다. 강의는 한국어뿐이라 다른 언어에는 없는 기능이다. 두 입구를 한국어 화면에서만 보이게 한다.
「이 결과로 AI에게 물어보기」를 맨 위 아이콘 · 다이얼로그로 옮기고 STEP 5로 잇기, 다국어 열기
대표님 11/5. ① 요약 탭 카드를 빼고 결과 머리 공유 버튼 오른쪽 AI 아이콘 → 다이얼로그 ② 다음 단계 STEP 5를 「내 AI 도구와 대화하기」로 바꾸고 「자세히 보기」도 다이얼로그 ③ 친구 순서 1번 「친구와 카톡으로 결과를 보내거나 받아요」 ④ 다국어 번역(다른 언어는 카톡 → 메신저, 109는 한국어에만, 분량은 언어에 맞게). 한국어 고민 상담의 109 문구는 그대로 둔다.