백로그
1Cloudflare CDN 설정
## 문제 정적 자산(JS, CSS, 이미지)이 Rails 서버에서 직접 서빙. 모든 요청이 서버에 도달. ## 변경 (코드 외 인프라 작업) 1. Cloudflare에 9way.org 도메인 등록 (무료 플랜) 2. DNS를 Cloudflare 네임서버로 변경 3. SSL: Full(Strict) 모드 설정 (Kamal Proxy Let's Encrypt와 공존) 4. 캐싱 규칙: `/assets/*` → Cache Everything, Edge TTL 30일 5. Page Rules: `/up` → Bypass Cache (health check) ## 리스크 - Cloudflare + Kamal Proxy 이중 SSL → Full(Strict) 모드 필수 (리다이렉트 루프 방지) ## 완료 기준 - [ ] 정적 자산 CDN 서빙 (cf-cache-status: HIT) - [ ] SSL 정상 작동 - [ ] 진단 플로우 정상 동작
할 일
0진행 중
0리뷰
0완료 (15일)
104Backend - 멤버 Top 10 DNA 데이터 확장
## 목표 TeamStrengthAnalyzer에서 멤버별 Top 10 DNA 데이터를 제공하여 개요 탭의 WAY×DNA 강점 표를 지원한다. ## 변경 파일 - `app/services/workspaces/team_strength_analyzer.rb` ## 상세 내용 1. `build_member_data` (L72-73): `first(5)` → `first(10)`으로 확장 2. `build_member_strength_map` (L197-219): 각 DNA에 `:tier` 필드 추가 - `:top5` (rank 1-5), `:top10` (rank 6-10), `nil` (그 외) - 기존 `top5` 필드는 하위호환을 위해 유지 3. `compute_team_dna_ranking` (L148-180): 이미 올바른 카운팅 로직 → 변경 불필요 ## 완료 기준 - [ ] 멤버 데이터에 Top 10 DNA가 포함됨 - [ ] strength_map의 각 DNA에 tier 정보가 정확히 표시됨 - [ ] 기존 matrix 탭이 정상 동작함 (하위호환) - [ ] 테스트 통과
Domain 상세 페이지 - 1위 중심 레이아웃 + 비율 바차트
## 목표 `app/views/diagnoses/pdf/_domain_detail.html.erb`를 개선하여 1위 도메인 중심 레이아웃 + 3영역 비율 바차트를 추가 ## 상세 작업 ### A. 3영역 비율 바차트 추가 (페이지 상단) - `result[:domain_chart]`에서 3개 domain의 score를 사용 - 비율 계산: `(각 domain score / 전체 합) × 100` → 소수점 반올림 - 정규화: 최대 비율을 50%로 정규화하여 바 너비 결정 - 수평 바차트로 렌더링 (각 행: 도메인명 | 바 | 실제%) - 색상: Thinking #3B82F6, Relationship #10B981, Execution #F59E0B (ChartHelper::DOMAIN_HEX 사용) ### B. 1위 Domain 중심 레이아웃 - 현재: 3개 domain이 동등하게 표시됨 (is_top만 highlight-card) - 변경: 1위 Domain을 상단에 크게 표시, 나머지 2개는 하단에 작게 - 1위 Domain에 상세 설명 추가: `I18n.t("scoring.9way_27dna.domains.{key}.detailed_description", default: nil)` - 1위 domain의 sub-ways는 유지 ### C. 나머지 2개 Domain 축소 - 2, 3위 domain은 compact 레이아웃으로 가로 배치 - 점수바 + way breakdown만 간략히 표시 ## 사용 가능한 데이터 - `result[:domain_chart]` → [{key: "thinking", name: "사고", score: 32.1}, ...] - `result[:top_domain]` → {key: "thinking", name: "사고", score: 15.0} - `result[:way_chart]` → [{key: "ideation", name: "발상", score: 12.5}, ...] - `ChartHelper::DOMAIN_HEX` → 색상 맵 - `ChartHelper::WAY_DOMAIN_MAP` → WAY→Domain 매핑 ## 스타일 참고 - 기존 PDF CSS 클래스 사용: highlight-card, card, bar-bg, bar-fill, bar-fill-{domain}, flex-between, text-md, text-xs 등 - PDF 레이아웃이므로 print 호환 필요 (Grover로 PDF 생성) ## 완료 기준 - 1위 Domain이 눈에 띄게 강조됨 - 3영역 비율 바차트가 상단에 렌더링됨 - 상세 설명 문단이 1위 Domain에 포함됨 - 기존 페이지 번호/footer 유지
PDF 리포트 재설계 - 표지 (1p)
## 섹션 1: 표지 (1페이지) 기존 `_cover.html.erb` 정제. 불필요한 장식 제거, 전문적 인쇄물 느낌. ### 포함 요소 - 9WAY 로고/브랜드 - 수검자 이름 - 진단 유형 (성인/대학생/학생/리더/부모) - 진단 완료일 - 퍼스널 브랜딩 문구 ("나는 ~한 ~한 사람입니다") - FUTURE TALENT INSTITUTE ### 디자인 원칙 - 박스/컬러 최소화 - navy + slate 타이포그래피 중심 - 깔끔한 구분선으로 구조화 ### 완료 기준 - 기존 cover 파트셜 리팩토링 - 4개 언어 i18n 지원 (ko/en/zh/vi)
PDF 리포트 재설계 - 오케스트레이터 + 스타일 정리
## pdf.html.erb 오케스트레이터 재구성 + CSS 정제 ### 오케스트레이터 (pdf.html.erb) 새 섹션 순서로 재구성: 1. 표지 (1p) 2. 강점 해석 가이드 (1p) 3. 요약 (1p) 4. DNA 차별성 Top5 (10p, loop) 5. 강점 프로파일 (2p) 6. 탁월한 역할 (2p) 7. AI 분석 (2p, 조건부) ### CSS 정제 (layouts/pdf.html.erb) - highlight-card, card-navy 등 불필요한 박스 스타일 제거 - 전문적 리포트 스타일 강화 - 구분선(border-bottom) + 타이포그래피 위계 중심 - 페이지 번호(page_num) 자동 계산 로직 ### 미사용 파트셜 정리 - `_way_detail.html.erb` (27 DNA 랭킹 테이블) → 삭제 또는 보관 판단 - `_way_map.html.erb` → 강점 프로파일에 통합되면 삭제 ### 완료 기준 - pdf.html.erb가 새 구조 반영 - CSS에서 미사용 클래스 제거 - 전체 PDF 생성 테스트 통과
강점 브랜딩: DB 마이그레이션 + 모델
## 목표 DiagnosisSession에 custom_branding JSONB 필드를 추가하여 사용자 커스텀 브랜딩 데이터를 저장합니다. ## 작업 내용 1. **마이그레이션 생성**: `add_custom_branding_to_diagnosis_sessions` - `custom_branding` JSONB 컬럼 추가 (default: {}) 2. **모델 업데이트**: `DiagnosisSession` - `custom_branding` 접근자 메서드 - 브랜딩 데이터 구조 검증 (selected_dna_keys, modifiers, selected_way_key, aspiration, ai_sentence) 3. **헬퍼 메서드**: - `branding_sentence`: AI 생성 문장 또는 기본 브랜딩 반환 - `has_custom_branding?`: 커스텀 브랜딩 존재 여부 ## JSONB 구조 ```json { "selected_dna_keys": ["creativity", "flexibility"], "modifiers": { "creativity": "혁신적인", "flexibility": "유연한" }, "selected_way_key": "ideation", "aspiration": "새로운 가능성을 여는 사람", "ai_sentence": "AI가 다듬은 최종 브랜딩 문장", "updated_at": "2026-02-27T..." } ``` ## 완료 기준 - [ ] 마이그레이션 성공 (docker compose exec web rails db:migrate) - [ ] DiagnosisSession#branding_sentence 동작 - [ ] 기존 세션 데이터에 영향 없음 확인 - [ ] 테스트 작성
사용자 진단
http://localhost:3000/admin/users/96c25ee8-34ba-4a3a-89af-d26f4ac0d653 사용자 진단 이력에서 진단 유형과 DNA 5개 결과, 결제 상태, 메모 등이 보여야해. 유료를 결제 상태로 바꿀 수도 있어야해.
사용자 상세 - 진단 이력 확장 (DNA 5개, 결제상태 변경, 메모)
## 목표 admin/users/show 페이지의 진단 이력 섹션을 확장합니다. ## 현재 상태 - `app/views/admin/users/show.html.erb` 55-80행에 기본 진단 이력 있음 (slug, 날짜, status만 표시) - `app/controllers/admin/users_controller.rb` show 액션에서 `@diagnosis_sessions` 로드 ## 변경 사항 ### 1. 컨트롤러 수정 (`app/controllers/admin/users_controller.rb`) - `show`에서 `includes(:diagnosis_type, :payment)` 추가 - `includes`에 `diagnosis_type: [:category, :target]`도 포함 ### 2. 뷰 수정 (`app/views/admin/users/show.html.erb`) 진단 이력 테이블에 다음 컬럼 추가: - **진단 유형**: `session.diagnosis_type.slug` (예: "strength_adult") - **Top 5 DNA**: `session.json_data["scored_dna"]`에서 상위 5개 DNA name 표시 - `json_data["scored_dna"]`가 배열로 [{key, name, score}, ...] 형태 - 없으면 `json_data.dig("top_dna")` 확인 - 각 DNA name을 작은 badge로 표시 - **결제 상태**: 결제 여부 badge + 변경 버튼 - `session.report_purchased?` → "유료" (green badge) - 미결제 → "무료" (gray badge) + "유료로 변경" 버튼 - 유료 → "무료로 변경" 버튼 (결제 해제) - **메모**: `session.memo` 표시 (truncate 50) - **완료일**: `session.end_time` 표시 ### 3. 결제 상태 변경 API - `admin/users_controller.rb`에 `toggle_payment` 액션 추가 - 라우트: `patch :toggle_payment` (member) - 로직: - 유료→무료: `session.update!(payment: nil)` (payment_id를 nil로) - 무료→유료: 새 Payment 생성 (free payment) ```ruby payment = Payment.create!( user: session.user, order_id: "admin_free_#{session.id}_#{Time.current.to_i}", amount: 0, status: "completed", payment_type: "report", currency: "KRW", payment_key: "admin_granted", approved_at: Time.current ) session.update!(payment: payment) ``` - Turbo를 사용하거나 단순 redirect_to로 처리 ### 4. 라우트 추가 (`config/routes.rb`) admin namespace 안에: ```ruby resources :users, only: %i[index show edit update] do member { patch :toggle_payment } end ``` 주의: 기존 `resources :users, only: %i[index show edit update]`를 block 형태로 변경 ## 스타일 참고 - 기존 admin 뷰 패턴 따르기 (Tailwind CSS) - badge: `inline-flex items-center px-2 py-0.5 rounded text-xs font-medium` - green: `bg-status-success-light text-status-success-text` - gray: `bg-gray-100 text-gray-600` - 버튼: `text-xs text-admin-primary hover:underline` ## 완료 기준 - 진단 이력에 유형, DNA 5개, 결제상태, 메모, 완료일 표시 - 결제 상태 토글 동작 (유료↔무료) - N+1 쿼리 없음
메인페이지(랜딩) 다국어 번역 오류 수정
## 문제 메인페이지(랜딩 페이지)의 번역이 제대로 적용되지 않음. ## 완료 기준 - 메인페이지의 모든 텍스트가 locale별(ko/en/zh/vi)로 올바르게 번역되어 표시됨 - 번역 누락된 키가 없음 - locale 전환 시 모든 섹션이 정상적으로 번역됨
관리자 메뉴
관리자는 사이드 메뉴에 관리자 페이지 이동하는 메뉴가 필요해.
사이드바에 관리자 메뉴 링크 추가
## 작업 내용 관리자(admin) 사용자에게만 보이는 "관리자" 메뉴 링크를 일반 사이드바에 추가합니다. ## 상세 요구사항 ### 1. sidebar_nav_item에 admin 아이콘 추가 - `app/views/shared/_sidebar_nav_item.html.erb`의 icons 해시에 "admin" 키 추가 - 톱니바퀴(settings/cog) 아이콘 SVG path 사용 ### 2. i18n 번역 추가 - `config/locales/ko.yml`의 `nav.sidebar` 아래에 `admin: "관리자"` 추가 - `config/locales/en.yml`의 `nav.sidebar` 아래에 `admin: "Admin"` 추가 - `config/locales/zh.yml`의 `nav.sidebar` 아래에 `admin: "管理员"` 추가 - `config/locales/vi.yml`의 `nav.sidebar` 아래에 `admin: "Quản trị"` 추가 ### 3. 사이드바에 관리자 메뉴 조건부 렌더링 - `app/views/shared/_sidebar.html.erb`에서 nav 섹션 끝(</nav> 직전), 나의 결과 아래에 추가 - `Current.user&.admin?` 조건으로 감싸기 - admin_root_path로 이동 - active 조건: `controller_path.start_with?("admin")` ### 4. 테스트 작성 - 관리자 사용자가 사이드바에서 "관리자" 링크를 볼 수 있는지 테스트 - 일반 사용자에게는 "관리자" 링크가 보이지 않는지 테스트 ## 완료 기준 - admin 사용자로 로그인 시 사이드바에 "관리자" 메뉴가 보임 - 일반 사용자로 로그인 시 "관리자" 메뉴가 보이지 않음 - "관리자" 클릭 시 /admin으로 이동 - 모든 테스트 통과
워크스페이스 메뉴 권한
사용자 권한이 코치 베이직~ 코치 마스터, 조직 베이직 ~ 조직 마스터, 관리자 ... 멤버를 제외한 모든 권한은 워크스페이스 메뉴 보여줘야해. 속한 워크스페이스가 없으면 워크스페이스 목록에 + 워크스페이스 생성이 있어야해. 그리고 소속된 워크스페이스 있어도 워크스페이스 생성은 모든 목록 밑에 보여야해.
사이드바 워크스페이스 섹션 권한 조건 변경 및 생성 링크 추가
## 작업 내용 ### 1. `_sidebar.html.erb` 수정 - 워크스페이스 섹션 표시 조건을 `@sidebar_workspaces&.any?` → `Current.user&.staff?`로 변경 - `staff?`는 `admin? || coach? || org_admin?`으로 이미 정의되어 있음 (멤버=regular 제외) - 워크스페이스가 없을 때와 있을 때 모두 워크스페이스 섹션이 보이도록 ### 2. `_sidebar_workspace_section.html.erb` 수정 - 워크스페이스 목록이 비어있을 때: "워크스페이스" 섹션 헤더 + "+ 워크스페이스 생성" 링크만 표시 - 워크스페이스 목록이 있을 때: 기존 목록 하단에 "+ 워크스페이스 생성" 링크 추가 - 생성 링크는 `new_workspace_path`로 연결 - 기존 사이드바 디자인 패턴(아이콘, 색상, padding)과 일관되게 구현 ### 3. 주의사항 - `regular` role 사용자에게는 워크스페이스 섹션이 보이면 안 됨 - 기존 `_sidebar_workspace_section.html.erb`의 워크스페이스 내 메뉴 권한 로직(admin/leader 조건)은 그대로 유지 - `set_sidebar_workspaces` 메서드는 변경 불필요 (빈 배열이 와도 섹션은 표시해야 함) ### 완료 기준 - staff 권한 사용자: 소속 워크스페이스 없어도 "워크스페이스" 섹션 + "워크스페이스 생성" 링크 표시 - staff 권한 사용자: 소속 워크스페이스 있으면 기존 목록 + 하단에 "워크스페이스 생성" 링크 표시 - regular 사용자: 워크스페이스 섹션 안 보임 (기존과 동일)
이메일 초대
http://localhost:3000/ko/workspaces/2dee7a1b-57c3-47b4-a40c-741d1aaccbbc/invitations 이메일 초대할 때, 해당 링크로 이메일을 실제로 발송되게 해줘. 그리고 이메일을 벌크로도 등록하고 발송할 수 있게 해줘. (그리고 해당 멤버가 멤버로 승인했는지도 표시해줘)
Admin Payments 컨트롤러 + 라우팅 + 테스트
## 작업 내용 Admin::PaymentsController를 생성하고, 결제 목록 조회 + 취소 기능을 구현합니다. ## 완료 기준 ### 1. 라우팅 (config/routes.rb) admin 네임스페이스에 추가: ```ruby resources :payments, only: [:index] do member do post :cancel end end ``` ### 2. Admin::PaymentsController (app/controllers/admin/payments_controller.rb) - `Admin::BaseController` 상속 - **index 액션:** - Payment.includes(:user).order(created_at: :desc) 로 기본 조회 - 검색: 사용자명/이메일/전화번호로 검색 (params[:search]) - 필터: 상태 (params[:status] - pending/completed/failed/canceled) - 필터: 결제 유형 (params[:payment_type] - report/career_up/premium/analysis_credit/package/workshop/strength_diary/custom) - 페이지네이션: PER_PAGE = 50, pagy 사용 (기존 컨트롤러들이 pagy 사용 중) - 정렬: created_at desc (기본), amount, status - **cancel 액션:** - 관리자 취소 기능. 기존 Payments::CancellationService를 참고하되, 관리자는 7일 제한 없이 취소 가능해야 함 - 취소 성공 시 flash notice와 함께 리다이렉트 - 취소 실패 시 flash alert와 함께 리다이렉트 ### 3. 테스트 (test/controllers/admin/payments_controller_test.rb) - index 액션 테스트: 관리자 접근 가능, 비관리자 접근 불가 - 검색 테스트 - 필터 테스트 - cancel 액션 테스트 - 기존 테스트 패턴: test/controllers/admin/ 디렉토리의 기존 테스트 참고 ### 참고 사항 - 기존 Admin::UsersController, Admin::DiagnosisSessionsController의 패턴을 따라주세요 - Payment 모델은 이미 존재합니다 (app/models/payment.rb) - 사용자 정보: user.name, user.email_address, user.phone - 상태 enum: pending, completed, failed, canceled - 결제 유형 enum: report, career_up, premium, analysis_credit, package, workshop, strength_diary, custom - pagy gem이 이미 설치되어 있습니다 - 기존 Admin::BaseController (app/controllers/admin/base_controller.rb) 상속 필수
페이지 이탈 분석 기능 구현
페이지뷰 추적 → 이탈 페이지/체류 시간/페이지 흐름 분석 기능을 admin 통계 대시보드에 추가. DB 마이그레이션, 모델, Concern, Job, Stimulus 컨트롤러, 통계 뷰, 테스트 포함.
할인코드
할인코드 테이블을 만들어야해. 할인코드 테이블을 만들고, 결제할 때 할인코드를 입력하면 할인된 금액을 결제하게 해야해. http://localhost:3000/admin 어드민에 할인코드 메뉴도 있어야함 할인코드는 상품별로 할인율을 정할 수 있어야함. (전체 적용도 가능) 할인코드 이름 / 할인코드 자동 생성 / 할인 유형 (원, %) / 적용 상품 / 최대 사용횟수 / 최소 구매 금액 / 만료일 / 활성화
할인코드 DB 마이그레이션 + 모델 + 서비스
## 목표 할인코드(DiscountCode) 테이블 생성, 모델 정의, 할인코드 검증/적용 서비스 구현 ## 작업 내용 ### 1. DB 마이그레이션 - `discount_codes` 테이블 생성: - name (string, NOT NULL) - 할인코드 이름 - code (string, NOT NULL, UNIQUE) - 할인코드 (자동생성 가능) - discount_type (string, NOT NULL) - 'percentage' 또는 'fixed_amount' - value (integer, NOT NULL) - 할인값 (퍼센트 또는 원) - applies_to_all (boolean, default: true) - 전체 상품 적용 여부 - max_uses (integer, nullable) - 최대 사용횟수 (null이면 무제한) - current_uses (integer, default: 0) - 현재 사용횟수 - min_purchase_amount (integer, default: 0) - 최소 구매금액 - expires_at (datetime, nullable) - 만료일 - active (boolean, default: true) - 활성화 여부 - metadata (jsonb, default: {}) - timestamps - 인덱스: code (unique), active, expires_at - `discount_code_products` 조인 테이블 생성: - discount_code_id (references, FK) - product_id (references, FK) - 인덱스: [discount_code_id, product_id] (unique) - `payments` 테이블의 discount_code_id를 UUID FK로 변환하는 마이그레이션 ### 2. 모델 - `DiscountCode` 모델: - enum discount_type: { percentage: "percentage", fixed_amount: "fixed_amount" } - has_many :discount_code_products, dependent: :destroy - has_many :products, through: :discount_code_products - has_many :payments - validates :name, :code, :discount_type, :value, presence: true - validates :code, uniqueness: true - validates :value, numericality: { greater_than: 0 } - validate: percentage는 1-100 범위 - scope :available - active && (expires_at nil OR 미래) && (max_uses nil OR current_uses < max_uses) - before_create :generate_code_if_blank (SecureRandom.alphanumeric(8).upcase) - `applicable_to?(product)`: applies_to_all이거나 products에 포함 - `calculate_discount(amount)`: 할인 금액 계산 - `usable?`: 사용 가능 여부 확인 - `use!`: current_uses 증가 - `DiscountCodeProduct` 모델: - belongs_to :discount_code - belongs_to :product - `Payment` 모델 수정: - belongs_to :discount_code, optional: true ### 3. 서비스 - `DiscountCodes::ValidationService`: - initialize(code:, product: nil, amount: 0) - call: 유효성 검사 (존재, 활성, 만료, 사용횟수, 최소금액, 상품 적용) - Result(discount_code, discount_amount, error) - `DiscountCodes::ApplyService`: - initialize(discount_code:, payment:) - call: 할인코드 사용 기록 (current_uses 증가, payment.discount_code_id 설정) ### 4. Fixture - test/fixtures/discount_codes.yml (active, expired, max_used, inactive 등) - test/fixtures/discount_code_products.yml ### 5. 테스트 - test/models/discount_code_test.rb - test/models/discount_code_product_test.rb - test/services/discount_codes/validation_service_test.rb - test/services/discount_codes/apply_service_test.rb ## 완료 기준 - 모든 마이그레이션 실행 성공 - 모델 테스트 통과 - 서비스 테스트 통과 - 기존 테스트가 깨지지 않음
9WAY에 doorkeeper 도입 (OAuth2 Provider)
## 개요 9WAY 강점 진단 서비스를 OAuth2 Provider로 전환하여 외부 서비스(커리어 관리, 채용, 교육)가 9WAY 계정으로 로그인할 수 있도록 한다. ## 작업 내용 - doorkeeper gem 설치 및 초기 설정 - doorkeeper 마이그레이션 실행 (oauth_applications, oauth_access_grants, oauth_access_tokens 테이블) - UUID primary key 설정 (기존 DB 패턴과 통일) - Authorization Code Grant 흐름 구현 - User 모델에 doorkeeper 관계 추가 - 기존 인증(Cookie Session, OmniAuth, Magic Link)과 병행 운영 - OAuth Application 관리 (어드민에서 client_id/secret 발급) ## 완료 기준 - [ ] doorkeeper 설치 및 마이그레이션 완료 - [ ] /oauth/authorize, /oauth/token 엔드포인트 동작 - [ ] 테스트 Consumer 앱에서 Authorization Code 흐름으로 로그인 성공 - [ ] 기존 웹 로그인(Google, Kakao, Magic Link) 정상 동작 확인 - [ ] 테스트 통과
OmniAuth Strategy (omniauth-9way) 작성 + 테스트
## 작업 내용 1. 기존 카카오 strategy 참고: `lib/omniauth/strategies/kakao.rb` 2. `lib/omniauth/strategies/nineway.rb` 생성: - OmniAuth::Strategies::OAuth2 상속 - option :name, 'nineway' - client_options: site, authorize_url(/oauth/authorize), token_url(/oauth/token) - ENV로 설정 가능하도록 (NINEWAY_CLIENT_ID, NINEWAY_CLIENT_SECRET, NINEWAY_SITE_URL) - info 콜백: /api/v1/me 호출하여 사용자 정보 가져오기 - uid: response의 id - info: name, email 3. Consumer 서비스에서 사용할 수 있도록 OmniAuth initializer 예시 작성 4. 테스트 작성: strategy의 기본 동작 검증 ## 참고 파일 - `lib/omniauth/strategies/kakao.rb` — 기존 커스텀 strategy 패턴 - `config/initializers/omniauth.rb` — OmniAuth 설정 패턴 - `config/initializers/doorkeeper.rb` — 스코프, 엔드포인트 확인 ## 완료 기준 - OmniAuth strategy 파일 작성 완료 - Consumer 서비스에서 바로 사용 가능한 수준 - 테스트 통과
Big5 성격 진단 기능 구현
Big5 성격 진단 전체 구현: 마이그레이션+모델+시드, 점수계산, 컨트롤러+라우트, 뷰+Stimulus, 결과 포매팅+결과뷰, i18n
Puma 2워커/5스레드 튜닝
## 목표 1000명 동시 진단 대응을 위해 Puma 동시 처리 능력을 3 → 10으로 향상. ## 변경 파일 - `config/deploy.yml` — `WEB_CONCURRENCY: 2`, `RAILS_MAX_THREADS: 5` 환경변수 추가 - `config/puma.rb` — `workers ENV.fetch("WEB_CONCURRENCY", 0)` + `preload_app!` 추가 ## 완료 기준 - 배포 후 Puma 2워커 기동 확인 (`ps aux | grep puma`) - 메모리 2GB 이내 확인 (`free -m`) - 헬스체크 정상 ## 주의사항 - **절대 3워커 이상 금지** — 4GB 서버에서 PDF 생성(Chromium) 시 OOM 위험 - DB 풀은 RAILS_MAX_THREADS 참조로 자동 반영 (워커당 5개) - PostgreSQL max_connections(100) 내 여유 확인: 2워커 × 5스레드 × 4DB = 최대 40 커넥션
Puma 워커/스레드 + DB 풀 설정
config/deploy.yml에 WEB_CONCURRENCY: 2, RAILS_MAX_THREADS: 5 추가. config/puma.rb에 workers + preload_app! 추가. 테스트 통과 확인.
Server: 일괄 제출 API + upsert_all + N+1 제거
## 목표 Big5 일괄 제출을 위한 서버 API 구현 + 기존 코드 N+1 제거. ## ⚠️ 리뷰 반려 사유 (2026-03-09) ### [CRITICAL] r[:selected] 타입 안전성 (controller:110) - `r[:selected] ? 1 : 0`에서 ActionController::Parameters를 통해 문자열 `"false"`가 올 경우 Ruby에서 truthy → 모든 응답이 1로 저장되는 치명적 버그 - **수정**: `ActiveModel::Type::Boolean.new.cast(r[:selected]) ? 1 : 0` 사용 ### [HIGH] question_id 타입 불일치 (controller:87-88) - `submitted_ids`가 문자열 Set, `valid_question_ids`가 UUID Set → 검증 무력화 가능 - **수정**: `r[:question_id].to_s`로 타입 통일 ### [HIGH] valid_questions_index 키 타입 불일치 (controller:107) - `index_by(&:id)` 키가 UUID인데 `r[:question_id]`가 문자열 → dna_type이 nil로 저장 - **수정**: `valid_questions_index[r[:question_id].to_s]` 또는 키 타입 통일 --- ## 작업 내용 (이하 기존 내용 동일) ### 1. 새 컨트롤러 액션: submit_all_big5_responses - POST /diagnosis_sessions/:id/submit_all_big5_responses - params: { responses: [{ question_id:, selected: true/false, response_time: }] } - 90개 응답(30라운드 × 3문항)을 한 번에 수신 - 서버 검증: 응답 수 확인, question_id 유효성, 세션 상태 확인 ### 2. session_manager: save_responses_bulk - upsert_all 활용 ### 3. N+1 제거 (기존 코드) ### 4. 라우트 추가 ## 완료 기준 - 반려 사유 3건 모두 수정 - 타입 안전성 테스트 추가 - 기존 테스트 통과
database.yml pool 키 수정 (max_connections → pool)
## 문제 `config/database.yml`에서 `max_connections` 키를 사용 중이나, Rails ActiveRecord가 인식하는 키는 `pool`임. 현재 AR 기본값 5가 우연히 맞아서 동작 중이나, 향후 스레드 수 변경 시 의도대로 동작하지 않을 위험. ## 변경 ```yaml # AS-IS max_connections: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> # TO-BE pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> ``` ## 완료 기준 - [ ] `pool` 키로 변경 - [ ] `docker compose exec web bin/rails runner "puts ActiveRecord::Base.connection_pool.size"` → 5 출력 - [ ] bin/ci 통과
PDF 리포트 재설계 - 강점 해석 가이드 (1p)
## 섹션 2: 강점 해석 가이드 (1페이지) 기존 `_way_map_guide.html.erb` 정제. 9WAY 체계를 읽기 쉽게 설명. ### 포함 요소 - 3가지 핵심 질문 (영역/역할/차별성) 설명 텍스트 - 9WAY Circle Chart (SVG, `way_map_svg` 헬퍼 활용) - 차트 읽는 법 간략 안내 ### 디자인 원칙 - 텍스트 중심, 5개 문단 → 더 간결하게 정제 가능 - Circle Chart 크기 적절히 배치 - 불필요한 배경색/박스 제거 ### 완료 기준 - 기존 way_map_guide 파트셜 리팩토링 - paragraph_1~5 i18n 키 정제
강점 브랜딩: 수식어/되고 싶은 모습 시드 데이터
## 목표 DNA별 수식어(modifier)와 WAY별 되고 싶은 모습(aspiration) 데이터를 생성합니다. ## 작업 내용 1. **시드 데이터 구조 설계** - DNA 27개 × 약 10개 수식어 (형용사/부사구) - WAY 9개 × 약 10개 되고 싶은 모습 (명사구) - 4개 언어 지원: ko, en, zh, vi 2. **저장 방식 결정** (2가지 옵션 중 택 1) - Option A: `branding_options` 테이블 신규 생성 (type: modifier/aspiration, key, locale, options[]) - Option B: i18n YAML 파일에 추가 (`config/locales/branding/ko.yml` 등) - 권장: Option B (별도 테이블 불필요, 배포 편리) 3. **데이터 작성** - 기존 `config/locales/dna/ko.yml`의 phrase/short 필드 참고 - DNA 수식어 예: Creativity → ["창의적인", "혁신적인", "독창적인", ...] - WAY 되고 싶은 모습 예: Ideation → ["새로운 가능성을 여는 사람", "아이디어로 세상을 바꾸는 사람", ...] ## 완료 기준 - [ ] 27 DNA × ~10 수식어 (ko) 작성 - [ ] 9 WAY × ~10 되고 싶은 모습 (ko) 작성 - [ ] en/zh/vi 번역 (최소 ko 완성 후) - [ ] 데이터 로드 확인
어드민_진단관리
http://localhost:3000/admin/diagnosis_types 진단결과에 유형, 사용자, 결제 상태 확인 가능해야 하고, 유료무료 변경 가능해야해. DNA 5개도 보여야 하고, 워크스페이스에 속해 있으면 속한 워크스페이스명도 보여야하고 진단유형 / 사용자 이름 / 사용자 이메일 / 유형 / 완료일 / 진단 결과 보기 / pdf 다운로드 / 결제상태(변경가능)
어드민 진단 세션 관리 목록 페이지 구현
## 목표 어드민에서 전체 진단 세션을 관리할 수 있는 목록 페이지를 새로 만듭니다. ## 새 파일 생성 ### 1. 컨트롤러: `app/controllers/admin/diagnosis_sessions_controller.rb` 기존 `admin/users_controller.rb` 패턴을 따르되: ```ruby module Admin class DiagnosisSessionsController < BaseController PER_PAGE = 50 def index @sessions = DiagnosisSession.includes(:user, :payment, :workspace_diagnoses, diagnosis_type: [:category, :target]) .where(status: "completed") .order(created_at: :desc) # 검색 (사용자 이름/이메일) if params[:q].present? q = "%#{params[:q]}%" @sessions = @sessions.joins(:user).where("users.name ILIKE :q OR users.email_address ILIKE :q", q: q) end # 진단 유형 필터 if params[:diagnosis_type_id].present? @sessions = @sessions.where(diagnosis_type_id: params[:diagnosis_type_id]) end # 결제 상태 필터 case params[:payment_status] when "paid" @sessions = @sessions.where.not(payment_id: nil).joins(:payment).where(payments: { status: "completed" }) when "free" @sessions = @sessions.where(payment_id: nil) end # 정렬 @sort = params[:sort].presence_in(%w[created_at end_time]) || "created_at" @dir = params[:dir].presence_in(%w[asc desc]) || "desc" @sessions = @sessions.order(@sort => @dir) # 페이지네이션 @page = [params[:page].to_i, 1].max @total_count = @sessions.count @total_pages = [(@total_count.to_f / PER_PAGE).ceil, 1].max @page = @total_pages if @page > @total_pages @sessions = @sessions.offset((@page - 1) * PER_PAGE).limit(PER_PAGE) @diagnosis_types = DiagnosisType.includes(:category, :target).order(:display_order) end def toggle_payment session = DiagnosisSession.find(params[:id]) if session.report_purchased? session.update!(payment: nil) else payment = Payment.create!( user: session.user, order_id: "admin_free_#{session.id}_#{Time.current.to_i}", amount: 0, status: "completed", payment_type: "report", currency: "KRW", payment_key: "admin_granted", approved_at: Time.current ) session.update!(payment: payment) end redirect_to admin_diagnosis_sessions_path(request.query_parameters.except(:action, :controller, :id)), notice: "결제 상태가 변경되었습니다." end end end ``` ### 2. 뷰: `app/views/admin/diagnosis_sessions/index.html.erb` 테이블 컬럼: 1. **진단 유형**: `session.diagnosis_type.slug` (category_target 형태) 2. **사용자 이름**: `session.user&.name` (링크 → admin_user_path) 3. **사용자 이메일**: `session.user&.email_address` 4. **Top 5 DNA**: `session.json_data["scored_dna"]` 상위 5개 name을 작은 badge로 - `json_data["scored_dna"]`가 [{key, name, score},...] 배열 - 없으면 `json_data.dig("top_dna")` 확인 5. **완료일**: `session.end_time&.strftime("%Y-%m-%d")` 6. **진단 결과 보기**: 링크 → `"/#{session.locale}/diagnoses/#{session.id}"` (새 탭) 7. **PDF 다운로드**: 링크 → `"/#{session.locale}/diagnoses/#{session.id}/download_pdf"` (새 탭) 8. **결제 상태**: badge + 변경 버튼 - 유료: green badge "유료" + "무료로 변경" 링크 - 무료: gray badge "무료" + "유료로 변경" 링크 - 변경은 PATCH toggle_payment 호출 9. **워크스페이스**: `session.workspace_diagnoses.first&.workspace&.name` (있으면 표시) 상단: - 검색바 (이름/이메일) - 진단 유형 필터 (select) - 결제 상태 필터 (전체/유료/무료) 하단: - 페이지네이션 (기존 admin 패턴 - `@page`, `@total_pages` 사용) ### 3. 라우트 추가 (`config/routes.rb`) admin namespace에: ```ruby resources :diagnosis_sessions, only: %i[index] do member { patch :toggle_payment } end ``` ## 스타일 참고 - 기존 `admin/users/index.html.erb` 테이블 패턴 따르기 - Tailwind CSS, admin 레이아웃 - `admin_helper.rb`의 `sort_link` 헬퍼 활용 ## 주의사항 - workspace_diagnoses에서 workspace 이름 가져올 때 N+1 방지: `includes(:workspace_diagnoses => :workspace)` 또는 lazy load - user가 nil일 수 있음 (optional: true) - nil 체크 필수 - WorkspaceDiagnosis 모델이 있는지 확인 (`app/models/workspace_diagnosis.rb`) ## 완료 기준 - /admin/diagnosis_sessions 에서 전체 진단 세션 목록 확인 가능 - 검색, 필터링, 페이지네이션 동작 - 결제 상태 토글 동작 - 워크스페이스 이름 표시
워크스페이스 메뉴 권한 테스트 작성
## 작업 내용 sidebar-dev가 구현을 완료한 후, 워크스페이스 메뉴 권한 관련 테스트를 작성합니다. ### 테스트 시나리오 1. **regular 사용자**: 워크스페이스 섹션이 사이드바에 표시되지 않아야 함 2. **coach_basic 사용자 (워크스페이스 없음)**: 워크스페이스 섹션 + "워크스페이스 생성" 링크 표시 3. **coach_basic 사용자 (워크스페이스 있음)**: 워크스페이스 목록 + 하단에 "워크스페이스 생성" 링크 표시 4. **org_admin_basic 사용자**: 위와 동일한 동작 5. **admin 사용자**: 위와 동일한 동작 ### 테스트 유형 - 시스템 테스트 또는 통합 테스트로 작성 - 기존 테스트 패턴을 따름 (test/system/ 또는 test/integration/) - Minitest + fixtures 사용 ### 확인할 파일 - `app/views/shared/_sidebar.html.erb` - 표시 조건 - `app/views/shared/_sidebar_workspace_section.html.erb` - 생성 링크 - `test/` 디렉토리의 기존 테스트 패턴 참고 ### 완료 기준 - 모든 권한별 시나리오 테스트 통과 - `bin/rails test` 전체 테스트 통과
결제 관리
http://localhost:3000/admin/ 관리자 메뉴에 결제 내용을 보여주는 메뉴를 만들어줘. 사용자, 이메일, 전화번호, 상품명, 가격, 상태, 할인금액, 결제일, 영수증, 취소 테이블 표시
Admin Payments 뷰 + 사이드바 메뉴
## 작업 내용 관리자 결제 관리 페이지의 뷰를 구현하고, 사이드바에 메뉴를 추가합니다. ## 완료 기준 ### 1. 사이드바 메뉴 추가 (app/views/admin/shared/_sidebar.html.erb) - "진단 세션" 메뉴 아래에 "결제 관리" 메뉴 추가 - 아이콘: Heroicon의 credit-card 또는 banknotes 아이콘 사용 - 링크: admin_payments_path - 활성 상태 표시: 기존 패턴 따르기 ### 2. Payments Index 뷰 (app/views/admin/payments/index.html.erb) 기존 admin 뷰 패턴(diagnosis_sessions/index.html.erb)을 참고하여 일관된 디자인으로 구현: **페이지 헤더:** - 제목: "결제 관리" - 전체 건수 표시 **검색 & 필터 영역:** - 검색: 사용자명/이메일/전화번호 통합 검색 - 상태 필터: 전체/대기중(pending)/완료(completed)/실패(failed)/취소(canceled) - 결제 유형 필터: 전체 + 각 payment_type (한글 표시) **테이블 컬럼 (요구사항 순서):** 1. 사용자 (user.name) 2. 이메일 (user.email_address) 3. 전화번호 (user.phone) 4. 상품명 (payment_type 한글 표시 - report: "강점 진단 리포트", analysis_credit: "분석 크레딧" 등) 5. 가격 (amount, number_to_currency 포맷) 6. 상태 (status, 색상 배지로 표시 - completed: green, pending: yellow, failed: red, canceled: gray) 7. 할인금액 (discount_amount, 0이면 "-") 8. 결제일 (approved_at, 없으면 "-") 9. 영수증 (receipt_url이 있으면 링크, 없으면 "-") 10. 취소 (취소 버튼 - completed 상태이고 canceled_at이 없을 때만 표시) **페이지네이션:** - pagy 사용 (기존 패턴 참고) **결제 유형 한글 매핑:** ``` report → 강점 진단 리포트 career_up → 커리어업 premium → 프리미엄 analysis_credit → 분석 크레딧 package → 패키지 workshop → 워크숍 strength_diary → 강점 다이어리 custom → 커스텀 결제 ``` **상태 한글 매핑:** ``` pending → 대기중 completed → 완료 failed → 실패 canceled → 취소 ``` ### 3. Tailwind CSS 스타일 - 기존 admin 뷰의 Tailwind 클래스 패턴을 정확히 따르세요 - 반응형 테이블 (overflow-x-auto) - 상태 배지, 버튼 등 기존 디자인 시스템 유지 ### 참고 사항 - 기존 admin 뷰 패턴: app/views/admin/diagnosis_sessions/index.html.erb 참고 - 사이드바: app/views/admin/shared/_sidebar.html.erb - Tailwind CSS 사용 중 - ERB 템플릿 엔진 - 한국어 UI
할인코드 어드민 CRUD + UI
## 목표 어드민 패널에 할인코드 관리 기능 구현 (CRUD + 토글) ## 작업 내용 ### 1. 라우트 (config/routes.rb) ```ruby # admin 네임스페이스 안에 추가 resources :discount_codes, except: :destroy do member do patch :toggle_active end end ``` ### 2. 컨트롤러 (app/controllers/admin/discount_codes_controller.rb) - Admin::BaseController 상속 - 기존 Admin::ProductsController 패턴 따름 - 액션: index, show, new, create, edit, update, toggle_active - Index: 검색(name, code), 필터(discount_type, active), 정렬, 페이지네이션 - PER_PAGE = 30 ### 3. 뷰 (app/views/admin/discount_codes/) - index.html.erb: 테이블 (이름, 코드, 유형, 값, 적용상품, 사용횟수/최대, 만료일, 활성화, 액션) - show.html.erb: 상세 정보 + 적용 상품 목록 + 사용 내역 - _form.html.erb: 생성/수정 공통 폼 - 이름 (text) - 코드 (text, 자동생성 안내) - 할인 유형 (select: percentage/fixed_amount) - 할인값 (number) - 전체 적용 (checkbox) - 적용 상품 (multi-select, applies_to_all이 false일 때만) - 최대 사용횟수 (number, optional) - 최소 구매금액 (number) - 만료일 (datetime) - 활성화 (checkbox) - new.html.erb, edit.html.erb: form 렌더 ### 4. 사이드바 (app/views/admin/shared/_sidebar.html.erb) - "Payments" 아래에 "Discount Codes" 메뉴 추가 - 아이콘: heroicon 'tag' 또는 유사 ### 5. 로케일 (config/locales/*.yml) - en.yml, ko.yml, zh.yml, vi.yml에 admin.discount_codes 섹션 추가 - 기존 admin.products 패턴 참고 ### 6. Tailwind CSS 스타일 - 기존 어드민 뷰 (payments, products) 스타일과 일관성 유지 - auto_submit_controller 사용 (검색/필터) ### 7. 테스트 (test/integration/admin/discount_codes_test.rb) - 접근 제어 (일반 사용자 차단) - Index (기본 조회, 검색, 필터, 정렬, 페이지네이션) - Create (성공, 실패) - Update (성공, 실패) - Toggle active - Show ## ⚠️ 의존성 - model-dev가 DiscountCode 모델과 마이그레이션을 완료해야 함 - 마이그레이션이 아직 없으면 먼저 `bin/rails db:migrate` 실행 후 작업 - 모델이 없으면 모델 파일이 생길 때까지 컨트롤러/뷰부터 작성 ## 파일 담당 범위 (이 파일들만 수정) - app/controllers/admin/discount_codes_controller.rb (신규) - app/views/admin/discount_codes/* (신규) - app/views/admin/shared/_sidebar.html.erb (수정) - config/routes.rb (수정 - admin 블록 안에 추가) - config/locales/en.yml, ko.yml, zh.yml, vi.yml (수정 - admin.discount_codes 추가) - test/integration/admin/discount_codes_test.rb (신규) ## 완료 기준 - 어드민에서 할인코드 CRUD 가능 - 사이드바에 메뉴 표시 - 4개 로케일 파일 업데이트 - 통합 테스트 통과
Resource API 구축 (/api/v1)
## 개요 doorkeeper Bearer 토큰 인증 기반의 Resource API를 구축하여 외부 서비스에서 진단 결과, 강점 프로필 등을 조회할 수 있도록 한다. ## 작업 내용 - api/v1 namespace 신규 생성 - doorkeeper Bearer 토큰 인증 기반 API base controller - Scope 기반 권한 설계 (profile, diagnoses, strengths) - API endpoints: - GET /api/v1/me — 현재 사용자 프로필 - GET /api/v1/users/:uid/diagnoses — 진단 이력 - GET /api/v1/users/:uid/strength_profile — 강점 프로필 - Client Credentials Grant 지원 (서버 간 통신용) - API 응답 JSON 직렬화 ## 완료 기준 - [ ] Bearer 토큰으로 API 호출 성공 - [ ] 유효하지 않은 토큰 시 401 응답 - [ ] Scope에 따른 접근 제어 동작 - [ ] Client Credentials Grant로 서버 간 API 호출 성공 - [ ] API integration test 통과
Consumer 서비스 연동 가이드 문서 작성
## 작업 내용 `docs/api/consumer_integration_guide.md` 연동 가이드 문서 작성 ### 포함할 내용 1. **개요**: 9WAY OAuth2 Provider 소개, 지원 Grant Type 2. **사전 준비**: - 9WAY 어드민에서 OAuth Application 등록 방법 - client_id, client_secret 발급 - redirect_uri 설정 - scopes 설명 (profile, email, diagnoses, strengths) 3. **Authorization Code Grant 흐름**: - 단계별 설명 (authorize → callback → token) - PKCE 지원 설명 - 코드 예시 (Rails + OmniAuth) 4. **Client Credentials Grant 흐름**: - 서버 간 통신용 - 토큰 발급 curl 예시 5. **API 호출 예시**: - GET /api/v1/me (프로필 조회) - GET /api/v1/users/:id/diagnoses (진단 이력) - GET /api/v1/users/:id/strength_profile (강점 프로필) - 각 응답 JSON 예시 포함 6. **사용자 식별**: - provider_uid 매핑 방법 - Consumer DB에 저장할 필드 7. **에러 처리**: - 401, 403, 404 응답 처리 - 토큰 갱신 (Refresh Token) 8. **Rails Consumer 빠른 시작**: - Gemfile 설정 - OmniAuth initializer - 콜백 컨트롤러 예시 - API 클라이언트 서비스 객체 예시 ## 참고 - 기존 doorkeeper 설정: `config/initializers/doorkeeper.rb` - API 컨트롤러: `app/controllers/api/v1/` - API 라우트: `config/routes.rb`의 api namespace ## 완료 기준 - 개발자가 문서만 보고 Consumer 서비스를 연동할 수 있는 수준 - curl 예시와 Rails 코드 예시 모두 포함
Big5 응답 일괄 제출 (레거시 패턴)
## 목표 Big5 진단 응답을 매 라운드 서버 전송 → JS 누적 후 마지막 1번 제출로 변경. HTTP 요청 30배 감소 (1000명: 30,000 → 1,000). ## 배경 (레거시 분석) 레거시(Next.js)는 모든 응답을 localStorage에 누적 → 마지막에 POST 1번으로 1000명 동시 진단 처리. 현재 Rails는 30라운드 × 개별 AJAX = 30배 더 많은 서버 요청 발생. ## 변경 범위 - **Stimulus 컨트롤러**: 응답 데이터를 JS 메모리/sessionStorage에 누적 - **Turbo Stream 라운드 전환**: 서버 저장 제거, 클라이언트에서 다음 라운드 전환 (UI 유지) - **diagnosis_sessions_controller**: 최종 제출 액션 추가 (submit_all_big5_responses) - **session_manager**: save_responses_bulk 메서드 (upsert_all 활용) - **sessionStorage 폴백**: 네트워크 끊김/새로고침 대비 임시 저장 ## 완료 기준 - Big5 진단 30라운드 완료 시 서버 요청 1회만 발생 - sessionStorage에 중간 응답 저장 확인 - 새로고침 시 진행 상태 복원 - 기존 테스트 통과 ## 리스크 - 네트워크 끊김 시 데이터 손실 → sessionStorage 폴백으로 대응 - 라운드별 서버 검증 제거 → 최종 제출 시 일괄 검증으로 보완 - submit_part2_round도 동일 패턴 적용 검토 ## 의존성 없음 (PR #1과 병렬 작업 가능)
진단 결과 Rails.cache.fetch 적용
diagnoses_controller.rb show에서 Big5/9Way/Engagement 결과를 Rails.cache.fetch로 캐싱. expires_in: 30.days. big5_result_formatter.rb의 find_quality_code 맵핑도 캐시. 테스트 통과 확인.
Client: Stimulus 컨트롤러 + sessionStorage + 라운드 전환
## 목표 Big5 진단의 라운드별 AJAX 제출을 클라이언트 누적 + 최종 1회 제출로 변경. ## ⚠️ 리뷰 반려 사유 (2026-03-09) ### [MUST-FIX] 접근성 ARIA 속성 누락 - 질문 카드에 `role="radio"`, `aria-checked` 없음 - 컨테이너에 `role="radiogroup"` 없음 - neither 버튼에 `aria-label` 없음 - 카운트다운에 `role="timer"`, `aria-live="polite"` 없음 - 선택 시 `aria-checked="true"` 상태 변경 필요 ### [MUST-FIX] JS 내 한국어 텍스트 하드코딩 - `_showSubmitting()`, `_showComplete()`, `_showError()` 내 텍스트가 한국어만 - 4개 언어(ko, en, zh, vi) 지원인데 i18n 미적용 - **수정**: Stimulus value로 번역 텍스트 전달 또는 서버 렌더링 `<template>` 사용 ### [MUST-FIX] 타임아웃 자동 스킵 피드백 부재 - 30초 경과 시 자동으로 다음 라운드로 넘어가지만 사용자 안내 없음 - 토스트 메시지 또는 안내 텍스트 필요 --- ## 작업 내용 (이하 기존 내용 동일) ### 1. Stimulus 컨트롤러: big5_batch_controller.js ### 2. 클라이언트 라운드 전환 ### 3. sessionStorage 폴백 ### 4. 뷰 수정 ## 완료 기준 - 반려 사유 3건 모두 수정 - ARIA 접근성 테스트 확인 - 4개 언어에서 UI 텍스트 정상 표시 - 기존 기능 동작 유지
prepare_all_big5_rounds 세션별 캐싱
## 문제 `diagnosis_sessions_controller.rb#prepare_all_big5_rounds`가 매 show 요청마다 실행 (300-500ms). 90개 문항 + 번역 로드 + 45라운드 생성. 세션 ID 기반 결정론적 RNG이므로 결과 항상 동일. ## 변경 ```ruby def prepare_all_big5_rounds Rails.cache.fetch(["big5_rounds", @session.id, I18n.locale], expires_in: 1.day) do # 기존 로직 end end ``` 또한 `load_questions`와 `prepare_all_big5_rounds`의 이중 쿼리 통합: - load_questions에서 한 번 로드한 것을 재활용하도록 리팩토링 ## 완료 기준 - [ ] show 페이지 캐시 히트 시 응답시간 300-500ms → 10-30ms - [ ] 새로고침 시 동일 라운드 데이터 반환 - [ ] bin/ci 통과
DiagnosesController 권한 로직 수정 - 워크스페이스 admin 멤버 결과 열람 허용
## 목표 워크스페이스 admin이 같은 워크스페이스 멤버의 진단 결과를 볼 수 있도록 DiagnosesController 권한 로직을 수정한다. ## 현재 문제 - `DiagnosesController#set_session`에서 `@session.user == Current.user` 소유자만 허용 - 워크스페이스 admin이 멤버 결과에 접근하면 "접근 권한이 없습니다" 에러 발생 ## 수정 사항 - `set_session`에서 워크스페이스 admin 권한 체크 추가 - `WorkspaceDiagnosis` 테이블로 해당 세션이 워크스페이스에 속하는지 확인 - 보안: 같은 워크스페이스의 admin만 허용 (leader, member 불가) ## 완료 기준 - [ ] 워크스페이스 admin이 멤버 진단 결과를 볼 수 있음 - [ ] 워크스페이스 leader/member는 여전히 타인 결과 접근 불가 - [ ] 워크스페이스 비멤버는 접근 불가 - [ ] 자기 자신의 결과는 기존처럼 볼 수 있음
PDF 리포트 재설계 - 요약 (1p)
## 섹션 3: 요약 (1페이지) 기존 `_executive_summary.html.erb` 축소/정제. ### 포함 요소 - 퍼스널 브랜딩 문구 - 1위 강점 영역 (Domain) + 설명 - 1위 강점 역할 (WAY) + 설명 - 1위 강점 차별성 (DNA) + 설명 - Top 5 DNA 리스트 (순위, 이름, 한줄 설명) ### 디자인 원칙 - highlight-card/card-navy 등 컬러 박스 최소화 - 깔끔한 텍스트 위계 (제목/소제목/본문)로 구조화 - 불필요한 synergy 카드 제거 고려 ### 완료 기준 - 기존 executive_summary 파트셜 리팩토링 - 1페이지에 깔끔하게 수렴
강점 브랜딩: 수정 위자드 UI (Stimulus 5-step modal)
## 목표 강점 브랜딩 수정을 위한 5단계 위자드 모달 UI를 구현합니다. ## 위자드 단계 1. **DNA 선택** (Top 5 중 1~3개 선택, 체크박스) 2. **DNA 수식어 선택** (선택한 각 DNA의 수식어 목록에서 택 1, 라디오) 3. **WAY 선택** (Top 3 중 1개 선택, 라디오) 4. **되고 싶은 모습 선택** (선택한 WAY의 aspiration 목록에서 택 1, 라디오) 5. **AI 브랜딩 미리보기** (선택 결과 + AI 생성 문장 표시, 수정/재생성 가능) ## 기술 스택 - **Stimulus Controller**: `branding_wizard_controller.js` - 참고: 기존 `role_wizard_controller.js` 패턴 활용 - step 전환, 선택 상태 관리, API 호출 - **Turbo Frame**: 모달 내용 동적 로드 - **디자인**: 기존 모달 스타일 (`way_contribution_modal` 참고) ## 화면 구성 - 상단: 진행 표시 (Step 1/5, 2/5, ...) - 중앙: 선택 UI (카드형 그리드) - 하단: 이전/다음 버튼 - 최종 단계: "저장" 버튼 ## 완료 기준 - [ ] 5단계 위자드 모달 동작 - [ ] 각 단계 선택 데이터 올바르게 전달 - [ ] 뒤로 가기 시 이전 선택 유지 - [ ] 모바일 반응형 - [ ] AI 호출 중 로딩 상태 표시
문항관리
http://localhost:3000/admin/survey_questions 문항관리는 문항 번호별로 관리하는게 아니라, 검사 유형 (강점검사), 검사 대상, 언어 이런식으로 목록이 있고, 해당 목록을 선택하면 해당 검사에 문항들이 part1 1~12 / part2 1~36번이 쭉 내용과 함께 보이는거야. part1은 선택지 1점 0 (0%) 2점 0 (0%) 이런식으로 그리고 part2는 선택지에 대해서 A선택지 0 (0%) B선택지 0 (0%) C선택지 0(%) 이런식으로 보여야해. 통계를 내야하거든.
문항관리 리디자인 - 유형/대상/언어 그룹 + 응답 통계
## 목표 기존 문항관리 페이지를 리디자인합니다. 문항 번호별 목록이 아닌, 검사 유형/대상/언어 그룹으로 목록을 보여주고, 선택하면 해당 문항들과 응답 통계가 표시됩니다. ## 변경 파일 ### 1. 컨트롤러 수정: `app/controllers/admin/survey_questions_controller.rb` #### index 액션 리디자인 - 기존 index: 문항 번호별 평면 목록 → 변경: 유형/대상/언어 그룹 목록 - 그룹 목록 데이터: ```ruby def index @groups = DiagnosisType.includes(:category, :target).where(active: true).order(:display_order) @locales = %w[ko en zh vi] end ``` - 각 그룹 = DiagnosisType × locale 조합 (예: "강점검사 - 성인 - 한국어") #### show 액션 (그룹 선택 시) - 라우트 변경 또는 새 액션 추가: `questions` 또는 기존 show 재활용 - 새 액션 `group_detail` 추가: ```ruby def group_detail @diagnosis_type = DiagnosisType.find(params[:diagnosis_type_id]) @locale = params[:locale] || "ko" @target = @diagnosis_type.target @questions = SurveyQuestion.includes(:translations, :category) .where(category_id: @diagnosis_type.category_id) .active .order(:part, :display_order) # 응답 통계 계산 # completed 세션의 response만 카운트 session_ids = DiagnosisSession.where(diagnosis_type: @diagnosis_type, status: "completed").pluck(:id) @response_stats = calculate_response_stats(@questions, session_ids) end ``` #### 응답 통계 계산 private 메서드 ```ruby def calculate_response_stats(questions, session_ids) return {} if session_ids.empty? stats = {} responses = DiagnosisResponse.where(diagnosis_session_id: session_ids, survey_question_id: questions.pluck(:id)) .group(:survey_question_id, :response_value) .count questions.each do |q| total = 0 distribution = {} if q.part == 1 # Part1: 1점~5점 분포 (1..5).each do |val| count = responses[[q.id, val.to_s]] || responses[[q.id, val]] || 0 distribution[val] = count total += count end else # Part2: A, B, C 선택지 분포 (response_value가 "A", "B", "C" 또는 1,2,3) # DiagnosisResponse의 response_value 확인 필요 # selected_option_id 또는 response_value로 구분 %w[1 2 3].each do |val| count = responses[[q.id, val]] || responses[[q.id, val.to_i]] || 0 distribution[val] = count total += count end end stats[q.id] = { distribution: distribution, total: total } end stats end ``` ### 2. 뷰 생성 #### `app/views/admin/survey_questions/index.html.erb` (리디자인) - 그룹 목록을 카드/테이블로 표시: - 진단 유형명 (category.name + target.name) - 언어 선택 탭 또는 버튼 (ko, en, zh, vi) - 문항 수 표시 - 클릭 시 → group_detail로 이동 #### `app/views/admin/survey_questions/group_detail.html.erb` (새 뷰) - 상단: 뒤로 가기 + 그룹 정보 (유형, 대상, 언어) - Part 1 섹션 (1~12번): ``` Q1. [문항 내용 - 해당 target/locale의 번역] 1점: 0 (0%) | 2점: 0 (0%) | 3점: 0 (0%) | 4점: 0 (0%) | 5점: 0 (0%) ``` - 각 점수의 count와 percentage 표시 - percentage = (count / total * 100).round(1) - 바 차트(간단한 inline width %)로 시각화 - Part 2 섹션 (1~36번): ``` Q1. [문항 내용] A선택지: [텍스트] 0 (0%) | B선택지: [텍스트] 0 (0%) | C선택지: [텍스트] 0 (0%) ``` - Part2 문항은 `SurveyQuestionTranslation`에서 해당 target, locale의 텍스트 가져오기 - 선택지 텍스트는 translation에 포함되어 있을 수 있음 - 확인 필요 ### 3. 라우트 추가 (`config/routes.rb`) ```ruby resources :survey_questions, except: :destroy do collection { get :group_detail } member do patch :soft_delete patch :restore end end ``` ## 모델 관계 참고 - `SurveyQuestion`: category_id, part(1 or 2), dna_type, display_order - `SurveyQuestionTranslation`: survey_question_id, target_id, locale, question_text - `DiagnosisResponse`: diagnosis_session_id, survey_question_id, response_value, selected_option_id - `DiagnosisType`: category_id, target_id, slug - `DiagnosisCategory`: name (예: "강점검사") - `DiagnosisTarget`: name, slug (예: "성인", "adult") ## 스타일 참고 - 기존 admin Tailwind 패턴 따르기 - 통계 바: `<div class="bg-admin-primary/20 rounded-full h-2"><div class="bg-admin-primary rounded-full h-2" style="width: X%"></div></div>` - 테이블 스타일은 기존 admin 패턴 ## 주의사항 - DiagnosisResponse의 response_value 형식을 실제 DB 데이터로 확인할 것 (docker compose exec web rails runner "DiagnosisResponse.limit(5).pluck(:response_value, :selected_option_id)") - Part2 선택지가 어떻게 저장되는지 확인 필요 - 대량 데이터 쿼리 시 성능 고려 (group/count로 집계) ## 완료 기준 - /admin/survey_questions 에서 유형/대상별 그룹 목록 표시 - 그룹 선택 시 문항 + 응답 통계 표시 - Part1: 1~5점 분포 (count + percentage) - Part2: A/B/C 선택지 분포 (count + percentage)
2
커스텀 결제 메뉴
관리자에 커스텀 결제 메뉴 있어야함 1. 커스텀 결제 링크 생성 2. 생성된 링크 관리 및 링크 복사, 취소, QR
사용자 관리 인덱스 개선 + 인라인 역할 수정 + 진단 뷰어
관리자 사용자 관리 페이지를 개선합니다. ## 요구사항 ### 1. 인덱스 테이블 컬럼 추가 현재 index에는 이름/이메일/역할/생성일만 표시됩니다. 다음 컬럼을 추가하세요: - **전화번호**: user_profiles 테이블의 phone_country_code + phone_number (예: +82 1012345678) - **소속 워크스페이스**: workspace_members를 통한 워크스페이스 이름 목록 (쉼표 구분) - **진단**: 진단 세션 수 (클릭 시 목록 표시) ### 2. 컨트롤러 수정 (app/controllers/admin/users_controller.rb) - index 액션에서 `includes(:profile, :workspaces, :diagnosis_sessions)` eager loading 추가 - 검색(q 파라미터)에서 전화번호 검색도 지원: `LEFT JOIN user_profiles ON user_profiles.user_id = users.id` 후 phone_number ILIKE 검색 추가 - `update_role` 액션 추가: PATCH 요청으로 역할만 업데이트, Turbo Stream으로 응답 ### 3. 인라인 역할 수정 - 역할 컬럼에서 역할 뱃지를 클릭하면 드롭다운(select)이 나타나도록 - Turbo Frame 또는 Stimulus 컨트롤러 사용 - 역할 선택 시 즉시 PATCH 요청으로 서버에 저장 - 성공 시 뱃지로 다시 표시 ### 4. 진단 목록 뷰어 - 진단 수를 클릭하면 해당 사용자의 진단 세션 목록이 표시되도록 - Turbo Frame으로 진단 목록을 로드하거나 모달 사용 - 진단 목록에 표시할 정보: 진단 유형, 상태, 완료일 ### 5. i18n - 새로 추가되는 텍스트의 번역 키를 config/locales/ko.yml에 추가 ## 주요 파일 - `app/controllers/admin/users_controller.rb` - `app/views/admin/users/index.html.erb` - `config/routes.rb` (update_role 라우트 추가 필요시) - `config/locales/ko.yml` - Stimulus 컨트롤러가 필요하면 `app/javascript/controllers/` 에 생성 ## 참고 - 전화번호는 users 테이블이 아닌 user_profiles에 있음 (phone_country_code, phone_number) - User 모델에 `has_one :profile, class_name: "UserProfile"` 연관 있음 - User 모델에 `has_many :workspaces, through: :workspace_members` 연관 있음 - 기존 role_label, role_badge_class 헬퍼가 이미 존재함 - 현재 정렬 가능 컬럼: name, email_address, role, created_at
결제 흐름에 할인코드 통합
## 목표 결제 페이지에서 할인코드를 입력하고 검증/적용하는 기능 구현 ## 작업 내용 ### 1. 할인코드 검증 API (app/controllers/discount_codes_controller.rb 신규) ```ruby # POST /discount_codes/validate class DiscountCodesController < ApplicationController def validate result = DiscountCodes::ValidationService.call( code: params[:code], product: find_product, # payment_type으로 product 조회 amount: params[:amount].to_i ) if result.success? render json: { valid: true, discount_amount: result.discount_amount, discount_code_id: result.discount_code.id, message: "할인이 적용되었습니다" } else render json: { valid: false, message: result.error } end end end ``` ### 2. 라우트 추가 (config/routes.rb) ```ruby # admin 블록 바깥에 추가 post "discount_codes/validate", to: "discount_codes#validate" ``` ### 3. 결제 흐름 수정 **PaymentsController#checkout 수정**: - checkout_params에 `discount_code_id` 추가 - discount_code_id가 있으면 DiscountCodes::ValidationService로 재검증 - 검증 성공 시 CheckoutService에 discount_amount 전달 - 결제 완료 후 DiscountCodes::ApplyService로 사용 기록 **PaymentsController#success 수정**: - 결제 확인 후 할인코드 사용 처리 (ApplyService) ### 4. Stimulus 컨트롤러 (app/javascript/controllers/discount_code_controller.js) ```javascript // 할인코드 입력 + 검증 + UI 업데이트 import { Controller } from "@hotwired/stimulus" export default class extends Controller { static targets = ["input", "result", "discountAmount", "discountCodeId", "totalAmount"] async validate() { const code = this.inputTarget.value.trim() if (!code) return const response = await fetch("/discount_codes/validate", { method: "POST", headers: { "Content-Type": "application/json", "X-CSRF-Token": document.querySelector("[name='csrf-token']").content }, body: JSON.stringify({ code, amount: this.originalAmount, payment_type: this.paymentType }) }) const data = await response.json() // UI 업데이트: 할인 금액 표시, hidden field 설정 } } ``` ### 5. 결제 뷰 수정 - 결제 페이지에 할인코드 입력 필드 추가 - 할인 적용 시 할인 금액과 최종 결제금액 표시 - hidden field로 discount_code_id 전달 ### 6. CustomPaymentsController 수정 - 커스텀 결제 링크에서도 할인코드 사용 가능하게 (선택사항) ### 7. 테스트 - test/controllers/discount_codes_controller_test.rb (validate 엔드포인트) - test/integration/payments_discount_test.rb (결제 + 할인코드 통합) ## ⚠️ 의존성 - model-dev가 DiscountCode 모델과 ValidationService를 완료해야 함 - 먼저 `bin/rails db:migrate` 실행하여 테이블 확인 - 모델/서비스가 없으면 컨트롤러/뷰/JS부터 작성 ## 파일 담당 범위 - app/controllers/discount_codes_controller.rb (신규) - app/javascript/controllers/discount_code_controller.js (신규) - app/controllers/payments_controller.rb (수정) - app/views/payments/ 관련 뷰 (수정) - config/routes.rb (수정 - discount_codes/validate 추가) - test/controllers/discount_codes_controller_test.rb (신규) - test/integration/payments_discount_test.rb (신규) ## 완료 기준 - 결제 페이지에서 할인코드 입력/검증 가능 - 유효한 코드 입력 시 할인 금액 표시 - 결제 완료 시 할인코드 사용 기록 - 테스트 통과
Consumer 연동 가이드 + 공통 OmniAuth Strategy
## 개요 외부 서비스(커리어, 채용, 교육)가 9WAY OAuth2 Provider에 쉽게 연동할 수 있도록 공통 OmniAuth Strategy와 연동 가이드를 제공한다. ## 작업 내용 - 9WAY용 OmniAuth strategy 작성 (omniauth-9way) - Consumer 서비스 boilerplate (Rails 기준) - 연동 가이드 문서 작성: - OAuth Application 등록 방법 - Authorization Code 흐름 설명 - API 호출 예시 (진단 결과 조회) - 사용자 식별 (provider_uid 매핑) ## 완료 기준 - [ ] OmniAuth strategy로 Consumer 앱에서 9WAY 로그인 성공 - [ ] API 토큰으로 진단 결과 조회 성공 - [ ] 연동 가이드 문서 완성
N+1 제거 + upsert_all + includes(:translations)
## 목표 일괄 제출된 응답을 효율적으로 저장. 쿼리 90개 → 1-2개로 감소. ## 변경 파일 - `app/services/diagnosis/session_manager.rb` — save_responses_bulk 메서드 추가 (upsert_all) - `app/controllers/diagnosis_sessions_controller.rb` — SurveyQuestion.find × N → where(id:).index_by - `app/controllers/diagnosis_sessions_controller.rb` — load_questions에 `.includes(:translations)` 추가 - submit_part2_round에도 동일 N+1 제거 적용 ## 구현 핵심 ```ruby # session_manager.rb def save_responses_bulk(session, responses_data) records = responses_data.map { |d| { diagnosis_session_id: session.id, ... } } DiagnosisResponse.upsert_all(records, unique_by: [:diagnosis_session_id, :survey_question_id]) end ``` ## 완료 기준 - 기존 테스트 통과 - DB 로그에서 최종 제출 시 쿼리 2개 이하 확인 - upsert_all 시 created_at 덮어쓰기 방지 확인 ## 주의사항 - upsert_all은 AR validation 스킵 → DB NOT NULL 제약으로 보장 - unique index `idx_responses_session_question` 활용 ## 의존성 PR #2 이후 (일괄 제출 구조에 맞춰 구현)
Big5Description 쿼리 캐싱
## 문제 `Big5ResultFormatter#build_description_cache`가 매 요청마다 `Big5Description.where(locale:)` 실행. 결과 페이지 자체는 Rails.cache로 캐싱되나, 캐시 미스 시 포맷팅 과정의 DB 쿼리는 캐싱 없음. ## 변경 ```ruby def build_description_cache(locale) @big5_cache = Rails.cache.fetch("big5:descriptions:#{locale}", expires_in: 1.day) do Big5Description.where(locale: locale) .each_with_object({}) do |desc, hash| hash[[desc.description_type, desc.key, desc.field_type]] = desc.content end end @name_suffix = locale == "ko" ? "ko" : locale.to_s end ``` ## 완료 기준 - [ ] Big5Description 쿼리가 locale별 1일 캐싱 - [ ] 캐시 미스 시에만 DB 쿼리 발생 - [ ] bin/ci 통과
Frontend - 개요탭 WAY×DNA 강점 매트릭스 표
## 목표 팀 강점 분석 개요 탭에 WAY×DNA 강점 매트릭스 표를 추가한다. ## 변경 파일 - `app/views/workspaces/team_strengths/_overview.html.erb` - `config/locales/ko.yml` (및 en, zh, vi) ## 표 구조 - **열**: 9 WAY (발상, 탐색, 평가, 연결, 조율, 촉진, 추진, 실행, 완결) - 각 WAY 아래 3개 DNA 서브행 - **행**: 멤버별 1행 - **셀**: Top 1-5 → ● (진한 색), Top 6-10 → ◐ (연한 색), 없으면 빈칸 - **하단 요약행**: DNA별 팀 가중치 카운트 (Top 1-5 = 1개, Top 6-10 = 0.5개) ## UI 요구사항 - 기존 "팀 WAY 분포" 섹션 아래에 배치 - 가로 스크롤 지원 (overflow-x-auto) - 첫 열(멤버 이름) 고정 고려 (sticky) - 반응형: 모바일에서도 가로 스크롤로 사용 가능 ## 완료 기준 - [ ] 9 WAY × 27 DNA × N 멤버 매트릭스 표가 정상 렌더링됨 - [ ] Top 1-5 / Top 6-10 시각 구분이 명확함 - [ ] 하단 요약행에 DNA별 가중치 카운트 표시 - [ ] 점수는 표시하지 않음
PDF 리포트 재설계 - DNA 차별성 Top5 (10p)
## 섹션 4: DNA 차별성 5개 설명 (각 2페이지 × 5 = 10페이지) 기존 `_dna_detail.html.erb` 디자인 정제. ### 각 DNA 2페이지 구성 **Page A:** - 순위 + DNA 이름 + 한줄 설명 - Definition (정의) - 강점 특징 (development) - 활용 방법 (use) **Page B:** - 동기부여 욕구 (desire) - Positive / Negative 행동 (2컬럼) - DNA 조합 (Top5 내 다른 DNA와의 시너지) ### 디자인 원칙 - detail-box 스타일 → 구분선 + 들여쓰기 기반 텍스트로 전환 - green/red 박스 → 최소한의 색상 힌트 (텍스트 컬러 정도) - combo-box → 간결한 리스트 형태 - 전문 리포트 느낌의 타이포그래피 ### 완료 기준 - 기존 dna_detail 파트셜 리팩토링 - Top5 각 2페이지, 총 10페이지 - 데이터 구조 변경 없음 (ResultFormatter 그대로 활용)
강점 브랜딩: 컨트롤러 API 엔드포인트
## 목표 브랜딩 수정 위자드에서 사용할 API 엔드포인트를 구현합니다. ## 엔드포인트 설계 1. **GET /diagnoses/:id/branding/edit** - 위자드 모달 렌더링 - Top 5 DNA, Top 3 WAY 데이터 포함 - 기존 커스텀 브랜딩이 있으면 선택 상태 복원 2. **GET /diagnoses/:id/branding/modifiers** - 선택한 DNA의 수식어 목록 반환 - params: dna_keys[] (배열) - Turbo Frame 또는 JSON 응답 3. **GET /diagnoses/:id/branding/aspirations** - 선택한 WAY의 되고 싶은 모습 목록 반환 - params: way_key - Turbo Frame 또는 JSON 응답 4. **POST /diagnoses/:id/branding/generate** - AI 브랜딩 문장 생성 요청 - params: selected_dna_keys[], modifiers{}, selected_way_key, aspiration - JSON 응답: { sentence: "AI 생성 문장" } 5. **PATCH /diagnoses/:id/branding** - 최종 브랜딩 저장 - custom_branding JSONB 업데이트 - 성공 시 요약 탭으로 리다이렉트 ## 라우팅 ```ruby resources :diagnoses, only: [] do resource :branding, only: [:edit, :update], controller: 'diagnoses/brandings' do get :modifiers get :aspirations post :generate end end ``` ## 완료 기준 - [ ] 5개 엔드포인트 동작 - [ ] 권한 체크 (본인 진단만 수정 가능) - [ ] Strong parameters 설정 - [ ] 에러 핸들링 (AI 실패 시 fallback) - [ ] 테스트 작성
사용자 관리
https://9way.org/admin 사용자 관리에서, 이름 / 이메일 / 전화번호 / 역할 / 소속 워크스페이스 / 생성일 / 진단 역할은 누르면 수정 가능하게 목록 상자로 진단은 누르면 해당 사용자가 진단한 목록들 볼 수 있게
사용자 관리 기능 테스트 작성
사용자 관리 기능의 테스트를 작성합니다. ## 테스트 범위 ### 1. 컨트롤러 테스트 (test/controllers/admin/users_controller_test.rb) - index 액션: 전화번호 검색이 정상 동작하는지 - index 액션: 워크스페이스, 진단 수가 올바르게 표시되는지 - update_role 액션: 역할 변경이 정상 동작하는지 - update_role 액션: 권한 없는 사용자가 접근 시 거부되는지 ### 2. 통합 테스트 (test/integration/admin/users_test.rb) - admin이 아닌 사용자가 접근 시 리다이렉트 - 인덱스 페이지에 전화번호, 워크스페이스, 진단 수가 표시되는지 - 역할 변경 플로우 ## 참고사항 - 기존 테스트 파일이 있으면 확장, 없으면 새로 생성 - fixtures 확인 후 적절한 테스트 데이터 사용 - Rails Minitest 사용 (RSpec 아님) - 대시보드에 테스트 결과 기록할 것 ## 의존성 - impl-dev의 구현이 완료된 후 작업 시작
doorkeeper 설치 + DB 마이그레이션 + 설정 + 인증 통합
## 작업 내용 1. Gemfile에 `gem 'doorkeeper'` 추가 및 bundle install 2. `rails generate doorkeeper:install` 실행 3. doorkeeper 마이그레이션 생성 — **UUID PK 필수** (기존 DB 패턴: pgcrypto, gen_random_uuid) 4. `rails db:migrate` 실행 5. `config/initializers/doorkeeper.rb` 설정: - `resource_owner_authenticator`: 기존 authentication.rb의 세션 기반 인증 활용 - `admin_authenticator`: 기존 admin 권한 체크 활용 - `grant_flows`: ['authorization_code', 'client_credentials'] - `access_token_expires_in`: 2.hours - `use_refresh_token`: true 6. `config/routes.rb`에 `use_doorkeeper` 추가 (기존 라우트와 충돌 없도록) 7. User 모델에 doorkeeper 관계 추가 8. 기존 인증(Cookie Session, OmniAuth, Magic Link)이 정상 동작하는지 확인 ## 참고 파일 - `app/controllers/concerns/authentication.rb` — 기존 인증 로직 - `app/controllers/concerns/authorization.rb` — 어드민 권한 - `app/models/user.rb` — User 모델 - `app/models/session.rb` — Session 모델 - `config/initializers/omniauth.rb` — OmniAuth 설정 - `config/routes.rb` — 라우트 ## 완료 기준 - doorkeeper 테이블 생성 완료 (UUID PK) - /oauth/authorize, /oauth/token 엔드포인트 접근 가능 - 기존 웹 로그인 정상 동작 - doorkeeper initializer 설정 완료
API Base Controller + routes + /me endpoint
## 작업 내용 1. `Api::V1::BaseController` 생성 (`app/controllers/api/v1/base_controller.rb`): - `ActionController::API` 상속 - `before_action :doorkeeper_authorize!` - JSON 에러 핸들링 (401, 403, 404, 422) - `current_resource_owner` 헬퍼 (doorkeeper token → User) 2. `config/routes.rb`에 API namespace 추가: ```ruby namespace :api do namespace :v1 do resource :me, only: [:show], controller: 'profiles' resources :users, only: [] do resource :diagnoses, only: [:show], module: 'users' resource :strength_profile, only: [:show], module: 'users' end end end ``` 3. `Api::V1::ProfilesController` 생성 (`GET /api/v1/me`): - doorkeeper scope: `:profile` - 현재 토큰 소유자의 프로필 반환 - 응답: id, email, name, locale, role, email_verified_at, created_at 4. CORS 설정 (rack-cors gem 또는 수동): - API 경로만 CORS 허용 ## 참고 파일 - `app/models/user.rb` — User 모델 구조 확인 - `config/initializers/doorkeeper.rb` — 스코프 설정 확인 - `config/routes.rb` — 기존 라우트 확인 ## 완료 기준 - GET /api/v1/me가 Bearer 토큰으로 동작 - 토큰 없이 요청 시 401 응답 - JSON 에러 핸들링 동작
Big5 결과 페이지 캐싱
## 목표 완료된 진단 결과(불변 데이터)를 Rails.cache로 캐싱하여 반복 조회 시 DB 쿼리 0개. ## 변경 파일 - `app/controllers/diagnoses_controller.rb` — show 액션에 Rails.cache.fetch 적용 - `app/services/diagnosis/big5_result_formatter.rb` — find_quality_code 맵핑 캐시 ## 구현 ```ruby # diagnoses_controller.rb @result = Rails.cache.fetch(["big5_result", @session.id, I18n.locale], expires_in: 30.days) do Diagnosis::Big5ResultFormatter.new(@session).format end ``` ## 완료 기준 - 첫 조회 시 Formatter 실행, 이후 캐시 히트 (로그 확인) - 9Way, Engagement 결과에도 동일 패턴 적용 - expires_in: 30일 (진단 결과는 불변) ## 의존성 없음 (독립 작업 가능)
response_timeout 120초 → 60초 축소
## 문제 Kamal Proxy의 response_timeout이 120초로, 느린 요청이 Puma 스레드를 2분간 점유 가능. 10개 동시 스레드에서 1개가 120s 묶이면 10% 용량 손실. ## 변경 ```yaml # config/deploy.yml proxy: ssl: true host: 9way.org response_timeout: 60 # 120 → 60 ``` 60초로 설정하는 이유: PDF 생성(grover gem)이 10-30초 소요 가능하므로 30초는 위험. ## 완료 기준 - [ ] deploy.yml response_timeout 60으로 변경 - [ ] kamal deploy 후 정상 동작 확인 - [ ] 진단 완료 + 결과 페이지 + PDF 생성 정상 작동
멤버 목록/카드 뷰에 결과 보기 버튼 추가
## 목표 워크스페이스 멤버 목록 페이지에서 admin이 멤버 결과를 바로 볼 수 있는 버튼을 추가한다. ## 현재 문제 - 목록형(_member_row.html.erb): 보기 버튼이 전혀 없음 (삭제 버튼만 존재) - 카드형(_member_card.html.erb): 자기 자신은 "나" 텍스트만 표시, 보기 링크 없음 ## 수정 사항 - `_member_row.html.erb`: admin일 때 보기 버튼 추가 - `_member_card.html.erb`: 자기 자신도 보기 링크 추가, admin이면 타인 결과 보기 링크 표시 - N+1 쿼리 방지를 위한 diagnosis_sessions 프리로드 검토 ## 완료 기준 - [ ] 목록형에서 admin에게 보기 버튼이 표시됨 - [ ] 카드형에서 자기 자신의 결과 보기 링크가 표시됨 - [ ] 카드형에서 admin이 타인 결과 보기 링크를 클릭하면 정상 동작 - [ ] 완료된 진단이 없는 멤버는 보기 버튼 비활성화/숨김
PDF 리포트 재설계 - 강점 프로파일 (2p)
## 섹션 5: 강점 프로파일 (2페이지) 기존 `_domain_detail.html.erb` + `_way_map.html.erb` 통합/재구성. ### Page A: Domain 비율 - 3개 강점 영역(사고/관계/행동) 비율 바차트 - 각 영역 설명 텍스트 - 하위 WAY 분포 ### Page B: 9WAY 분포 - 9가지 역할 막대그래프 (점수 높은 순) - 1위/2위 WAY 하이라이트 - 전체 분포 시각화 ### 디자인 원칙 - 도메인 컬러는 차트에서만 사용 (파란/초록/노랑) - 텍스트 설명은 navy/slate만 - 불필요한 배경색 박스 제거 ### 완료 기준 - 새 파트셜 `_strength_profile.html.erb` 생성 또는 기존 파트셜 통합 - 2페이지에 Domain + WAY 전체 분포 표현
강점 브랜딩: AI 브랜딩 문장 생성 서비스
## 목표 사용자 선택(DNA 수식어 + WAY 되고 싶은 모습)을 바탕으로 AI가 브랜딩 문장을 다듬어주는 서비스를 구현합니다. ## 서비스 설계 - **클래스**: `Diagnosis::BrandingGenerator` - **참고**: 기존 `Diagnosis::AiInsightGenerator` 패턴 활용 - **AI 클라이언트**: `Ai::Client` (Claude Sonnet 4.6) ## 입력 ```ruby { selected_dna: [{ key: "creativity", name: "창의력", modifier: "혁신적인" }, ...], selected_way: { key: "ideation", name: "발상", aspiration: "새로운 가능성을 여는 사람" }, user_name: "홍길동", locale: "ko" } ``` ## 출력 ```ruby { sentence: "혁신적이고 유연한 발상력으로 새로운 가능성을 여는 사람, 홍길동", alternatives: ["대안 문장 1", "대안 문장 2"] # 선택 옵션 } ``` ## 프롬프트 설계 - 수식어들을 자연스러운 한국어/영어 문장으로 연결 - 되고 싶은 모습을 문장 뒤에 배치 - 20자 이내의 간결한 문장 지향 - locale별 자연스러운 문체 ## 완료 기준 - [ ] AI 문장 생성 정상 동작 - [ ] 3개 대안 문장 제공 - [ ] 다국어 지원 (ko, en, zh, vi) - [ ] AI 실패 시 수식어+되고 싶은 모습 단순 조합으로 fallback - [ ] 응답 시간 3초 이내 - [ ] 테스트 작성 (AI mock)
문항 관리
문항관리에서 언어를 누르면 해당 질문이 나오는거지, 전체 메뉴 언어를 선택하는게 아니야. 영어를 선택하면 메뉴 전체가 영어로 바뀌네;;;
Mailer 생성 및 이메일 발송 연동
## 목표 이메일 초대 생성 시 실제 이메일이 발송되도록 구현 ## 구현 내용 1. WorkspaceInvitationMailer 생성 (app/mailers/workspace_invitation_mailer.rb) - invitation_email(invitation) 메서드 - 초대 링크 포함 (public_invitation_url) - 워크스페이스 이름, 역할 정보 포함 2. 이메일 템플릿 작성 - app/views/workspace_invitation_mailer/invitation_email.html.erb - app/views/workspace_invitation_mailer/invitation_email.text.erb - 기존 mailer.html.erb 레이아웃 활용 - 깔끔하고 전문적인 디자인 (기존 MagicLinkMailer 스타일 참고) 3. InvitationService.create! 수정 (app/services/workspaces/invitation_service.rb) - target_email이 있으면 WorkspaceInvitationMailer.invitation_email(invitation).deliver_later 호출 - 이메일 발송 실패해도 초대 생성은 유지 (rescue) 4. 이메일 재발송 기능 - InvitationsController에 resend 액션 추가 - config/routes.rb에 member route 추가: post :resend - _invitation.html.erb에 재발송 버튼 추가 5. 테스트 작성 - test/mailers/workspace_invitation_mailer_test.rb - InvitationService 테스트에 이메일 발송 테스트 추가 ## 담당 파일 (이 파일만 수정) - NEW: app/mailers/workspace_invitation_mailer.rb - NEW: app/views/workspace_invitation_mailer/invitation_email.html.erb - NEW: app/views/workspace_invitation_mailer/invitation_email.text.erb - MODIFY: app/services/workspaces/invitation_service.rb - MODIFY: config/routes.rb (resend route만 추가) - NEW: test/mailers/workspace_invitation_mailer_test.rb - MODIFY: test/services/workspaces/invitation_service_test.rb ## 완료 기준 - target_email 포함 초대 생성 시 이메일 발송됨 - letter_opener_web에서 발송된 메일 확인 가능 - 재발송 버튼으로 이메일 재발송 가능 - 테스트 통과
OAuth Application 어드민 관리 UI
## 작업 내용 1. 어드민 영역에 OAuth Application 관리 컨트롤러 생성 - `Admin::OauthApplicationsController` (CRUD) 2. 어드민 라우트 추가 (`admin/oauth_applications`) 3. 뷰 생성: - index: 등록된 OAuth Application 목록 (이름, client_id, redirect_uri, scopes) - new/edit: Application 등록/수정 폼 - show: Application 상세 (client_id, client_secret 표시, secret은 생성 시에만 표시) 4. 기존 어드민 레이아웃/스타일(Tailwind) 활용 5. 기존 어드민 사이드바에 메뉴 추가 ## 참고 파일 - `app/controllers/admin/` — 기존 어드민 컨트롤러 패턴 참고 - `app/views/admin/` — 기존 어드민 뷰 패턴 참고 - `app/views/layouts/admin.html.erb` — 어드민 레이아웃 - `app/helpers/icon_helper.rb` — 아이콘 헬퍼 ## 의존성 - backend-dev의 doorkeeper 설치 완료 후 작업 가능 - Doorkeeper::Application 모델 사용 ## 완료 기준 - 어드민에서 OAuth Application 생성/수정/삭제 가능 - client_id/secret 발급 및 표시 - scopes, redirect_uri 설정 가능
diagnoses + strength_profile API endpoints
## 작업 내용 1. 먼저 기존 모델 구조 파악: - `app/models/diagnosis_session.rb` — 진단 세션 모델 확인 - `app/models/` — 강점 관련 모델 확인 (strength, diagnosis_result 등) - 기존 뷰에서 진단 결과 어떻게 표시하는지 확인 2. `Api::V1::Users::DiagnosesController` 생성 (`GET /api/v1/users/:user_id/diagnoses`): - doorkeeper scope: `:diagnoses` - Client Credentials일 경우 params[:user_id]로 사용자 조회 - Authorization Code일 경우 현재 토큰 소유자만 허용 (또는 user_id 일치 확인) - 응답: 진단 이력 목록 (id, completed_at, status 등) 3. `Api::V1::Users::StrengthProfilesController` 생성 (`GET /api/v1/users/:user_id/strength_profile`): - doorkeeper scope: `:strengths` - 가장 최근 완료된 진단의 강점 프로필 반환 - 응답: top strengths, 점수, 완료 일시 등 4. JSON 직렬화: - jbuilder 사용 (프로젝트에 이미 있을 경우) 또는 `render json:` + `as_json` - 민감 정보 제외 (password_digest 등) ## 의존성 - backend-dev의 BaseController + routes 완료 후 작업 ## 참고 - `Api::V1::BaseController`를 상속 - doorkeeper_authorize! 는 BaseController에서 처리됨 - scope별 접근 제어: `before_action -> { doorkeeper_authorize! :diagnoses }` ## 완료 기준 - GET /api/v1/users/:id/diagnoses 동작 - GET /api/v1/users/:id/strength_profile 동작 - scope 미충족 시 403 응답 - Client Credentials + Authorization Code 모두 지원
Puma 스레드 5 → 7 증가
## 문제 현재 2 workers × 5 threads = 10 동시 슬롯. show 캐싱 후 대부분 I/O bound 작업이므로 스레드 증가가 효과적. ## 변경 ```yaml # config/deploy.yml env: clear: RAILS_MAX_THREADS: 7 # 5 → 7 ``` database.yml의 pool도 RAILS_MAX_THREADS를 참조하므로 자동 연동. 2 × 7 = 14 슬롯 (40% 동시성 증가). 스레드당 스택 ~1MB이므로 메모리 영향 미미 (~14MB 추가). ## 완료 기준 - [ ] RAILS_MAX_THREADS=7 설정 - [ ] DB pool이 7로 연동 확인 - [ ] 메모리 사용량 200MB 이내 증가 확인 - [ ] bin/ci 통과
유료 콘텐츠 PDF 파트셜 + 오케스트레이터 연동
## 목표 PDF에 유료 콘텐츠 페이지 추가 (report_purchased? 조건부 렌더링) ## 상세 작업 ### A. ResultFormatter 확인 및 데이터 매핑 - `app/services/diagnosis/result_formatter.rb` 확인 - `result[:summary_contents]`에 이미 field_type별 데이터가 있음: - student: job, course, study - college: job, resume, grade - adult: job, work, competency, team - leader: job, leadership, management, decision - StrengthDescription에서 field_type별 content 조회됨 (dna_key 기준) - 필요시 ResultFormatter에 premium_sections 메서드 추가 ### B. PDF 파트셜 생성: `app/views/diagnoses/pdf/_premium_content.html.erb` - `result[:summary_contents]`를 섹션별로 그룹핑하여 렌더링 - 각 field_type별 페이지: 1. **job 섹션**: 추천 직무 목록 + 각 직무별 상세 (별도 페이지) - 직무 상세 필드: 하는 일, 주요 과업, 직업 트렌드, 필요 지식, 필요 스킬, 필요 경험, 준비할 것 - content에서 `\n\n`으로 분리, 각 문단의 첫 줄이 필드 제목 패턴인지 판별 2. **기타 섹션** (course/study/resume/work 등): bullet 리스트로 표시 - 섹션 제목 + DNA명 + 콘텐츠 리스트 - 기존 PDF 스타일 클래스 사용 (page, section-header, card, text-* 등) ### C. 오케스트레이터 연동: `app/views/diagnoses/pdf.html.erb` - DNA detail 루프 뒤에 premium content 추가 - 조건: `<% if session.report_purchased? %>` - `<%= render "diagnoses/pdf/premium_content", session: session, result: result, page_num: (다음 페이지 번호) %>` ## 사용 가능한 데이터 - `result[:summary_contents]` → [{field_type: "job", content: "..."}, ...] - `result[:top_dna]` → 상위 5개 DNA 정보 - `session.report_purchased?` → payment_id.present? && payment.completed? - StrengthDescription.content_for(dna_key:, field_type:, locale:, target_slug:) ## 주의사항 - **수정 파일**: `_premium_content.html.erb`(신규), `pdf.html.erb`(추가만) - `_domain_detail.html.erb`는 건드리지 마세요 (다른 팀원 담당) - `result_formatter.rb`는 읽기만, 수정이 필요하면 최소한으로 - PDF CSS는 `app/views/layouts/pdf.html.erb`에 정의됨 - 읽어서 기존 클래스 활용 ## 완료 기준 - report_purchased?인 경우에만 유료 페이지가 PDF에 포함됨 - 직무별 상세가 개별 페이지로 렌더링됨 - 기타 섹션이 bullet 리스트로 정상 표시됨 - 미결제 시 유료 페이지가 PDF에 포함되지 않음
PDF 리포트 재설계 - 탁월한 역할 (2p)
## 섹션 6: 탁월한 역할 (2페이지) 기존 `_way_sections.html.erb` + `_premium_content.html.erb` 통합. ### Page A: 1위 WAY × DNA 조합 상세 - 강점 슬로건 - WAY×DNA 조합 설명 (sections 1-4) - 이 역할에서 탁월해지는 이유 ### Page B: target별 콘텐츠 - 직업/직무 추천 (job) - target에 따른 추가 콘텐츠 (adult: work/competency/team, student: course/study 등) - SUMMARY_FIELD_MAP 기반 동적 렌더링 ### 디자인 원칙 - highlight-card → 텍스트 위계로 대체 - detail-box-accent → 구분선 기반 - target별 콘텐츠가 없으면 Page B는 WAY 조합 sections 5-8로 채움 ### 완료 기준 - 기존 way_sections + premium_content 통합 - report_purchased? 조건 유지 (유료 콘텐츠 보호) - 2페이지에 깔끔하게 수렴
강점 브랜딩: 요약 탭 UI 연동 + PDF 반영
## 목표 커스텀 브랜딩 결과를 진단 결과 요약 탭과 PDF 리포트에 반영합니다. ## 작업 내용 ### 1. 요약 탭 (_tab_summary.html.erb) - 기존 브랜딩 섹션에 "수정" 버튼 추가 - 커스텀 브랜딩이 있으면 AI 생성 문장 표시 - 없으면 기존 자동 생성 브랜딩 표시 - "수정" 클릭 → 위자드 모달 열기 - "초기화" 버튼 → 기본 브랜딩으로 복귀 ### 2. PDF 리포트 - `_executive_summary.html.erb` (P3 요약 페이지) - 커스텀 브랜딩이 있으면 해당 문장 사용 - `session.branding_sentence` 헬퍼 활용 - `_cover.html.erb` (P1 표지) - 표지의 브랜딩 문구도 커스텀 반영 ### 3. 공유 로직 - `DiagnosisSession#branding_sentence` 메서드 하나로 통일 - custom_branding 있으면 → ai_sentence 반환 - 없으면 → 기존 ResultFormatter 기반 자동 생성 ## 완료 기준 - [ ] 요약 탭에서 커스텀 브랜딩 표시 - [ ] "수정" 버튼으로 위자드 진입 - [ ] PDF에 커스텀 브랜딩 반영 - [ ] 초기화 기능 동작 - [ ] 기존 브랜딩 없는 세션 영향 없음
통계 관리
어드민에 통계 관리 메뉴를 만들어줘. http://localhost:3000/admin?locale=ko 여기는 구글애널리틱스나 믹스패널처럼 이 서비스의 통계가 나와서 관리할 수 있어야해.
벌크 이메일 초대 기능
## 목표 여러 이메일을 한 번에 등록하고 초대 발송하는 벌크 초대 기능 구현 ## 구현 내용 1. InvitationsController에 bulk_create 액션 추가 - POST /workspaces/:workspace_id/invitations/bulk_create - 여러 이메일을 쉼표/줄바꿈으로 구분하여 받음 - 각 이메일에 대해 초대 생성 + 이메일 발송 - 성공/실패 카운트 flash 메시지 2. InvitationService에 bulk_create! 메서드 추가 - emails 배열, workspace, params(role, expires, group 등), created_by - 이메일 유효성 검사 (형식, 중복, 이미 초대된 이메일) - 각 이메일에 대해 create! 호출 (개별 에러 처리) - 결과 반환: { success: [...], failed: [...] } 3. 벌크 입력 UI (index.html.erb의 Email Invite 탭 수정) - textarea로 다수 이메일 입력 (쉼표 또는 줄바꿈 구분) - 기존 단일 이메일 입력도 유지 - "벌크 초대" 토글/탭으로 전환 - 입력된 이메일 수 실시간 카운트 표시 (Stimulus) 4. Stimulus 컨트롤러 (bulk_invite_controller.js) - textarea 입력 시 이메일 수 카운트 - 이메일 유효성 클라이언트 검증 5. 라우트 추가 - config/routes.rb에 collection route: post :bulk_create 6. 테스트 작성 - test/controllers/workspaces/invitations_controller_bulk_test.rb - InvitationService bulk_create! 테스트 ## 담당 파일 (이 파일만 수정) - MODIFY: app/controllers/workspaces/invitations_controller.rb (bulk_create 액션 추가) - MODIFY: app/views/workspaces/invitations/index.html.erb (벌크 UI 추가) - MODIFY: config/routes.rb (bulk_create route만) - NEW: app/javascript/controllers/bulk_invite_controller.js - MODIFY: app/services/workspaces/invitation_service.rb (bulk_create! 메서드만 추가) - NEW: test/controllers/workspaces/invitations_controller_bulk_test.rb ## 주의사항 - mailer-dev가 InvitationService.create!에 이메일 발송을 추가하므로, bulk_create!는 기존 create!를 내부적으로 호출하면 됨 - config/locales는 수정하지 마세요 (status-dev가 담당) ## 완료 기준 - 여러 이메일을 한 번에 입력하여 초대 생성 가능 - 각 이메일에 개별적으로 초대 이메일 발송됨 - 유효하지 않은 이메일은 건너뛰고 결과 보고 - 테스트 통과
doorkeeper 통합 테스트
## 작업 내용 1. Authorization Code Grant 흐름 테스트: - OAuth Application 등록 → authorize 요청 → callback → token 교환 - access_token으로 보호된 리소스 접근 - 잘못된 token으로 401 응답 확인 2. Client Credentials Grant 테스트: - client_id/secret으로 직접 token 발급 - 서버 간 API 호출 시나리오 3. 기존 인증 회귀 테스트: - Google OAuth 로그인 정상 동작 - Kakao OAuth 로그인 정상 동작 - Magic Link 로그인 정상 동작 - 기존 세션 기반 인증 정상 동작 4. Refresh Token 테스트: - 만료된 access_token으로 refresh 요청 - 새 access_token 발급 확인 ## 참고 파일 - `test/integration/authentication_test.rb` — 기존 인증 테스트 패턴 참고 - `test/test_helper.rb` — 테스트 헬퍼 ## 의존성 - backend-dev의 doorkeeper 설치 완료 후 작업 가능 ## 완료 기준 - 모든 테스트 통과 (rails test) - Authorization Code, Client Credentials 흐름 검증 - 기존 인증 회귀 테스트 통과
Resource API 통합 테스트
## 작업 내용 `test/integration/api/v1/` 디렉토리에 통합 테스트 작성 1. **ProfilesController 테스트** (`profiles_test.rb`): - Bearer 토큰으로 GET /api/v1/me 성공 - 토큰 없이 요청 시 401 - profile scope 없는 토큰으로 요청 시 403 - 응답 JSON 구조 검증 2. **DiagnosesController 테스트** (`diagnoses_test.rb`): - Authorization Code 토큰으로 자신의 진단 이력 조회 - Client Credentials 토큰으로 특정 사용자 진단 조회 - diagnoses scope 없는 토큰으로 403 - 존재하지 않는 사용자 404 3. **StrengthProfilesController 테스트** (`strength_profiles_test.rb`): - 강점 프로필 조회 성공 - strengths scope 없는 토큰으로 403 - 진단 결과 없는 사용자 처리 4. **공통 테스트**: - CORS 헤더 확인 - 만료된 토큰으로 401 - 잘못된 형식 토큰으로 401 ## 의존성 - backend-dev + api-dev 완료 후 작업 ## 참고 - `test/integration/doorkeeper_oauth_test.rb` — 기존 doorkeeper 테스트 패턴 참고 - Minitest 사용, fixtures 기반 ## 완료 기준 - 모든 테스트 통과 - 전체 `rails test` 통과 (기존 payments 버그 제외)
기존 팀 상위 DNA 섹션 정리
## 목표 개요 탭의 기존 "팀 상위 강점 DNA" 섹션을 카운팅 기반으로 정리한다. ## 변경 파일 - `app/views/workspaces/team_strengths/_overview.html.erb` ## 상세 내용 1. 현재 dna[:score]는 이미 가중치 카운트 (Top1-5=1.0, Top6-10=0.5) 2. 표시 형식 변경: "3.5" → 단위 명확화 또는 순위 배지로 개선 3. 점수 노출 제거, 시각적 강조로 대체 ## 완료 기준 - [ ] 점수 대신 가중치 카운트가 명확히 표시됨 - [ ] WAY×DNA 표와 시각적 일관성 유지
PDF 리포트 재설계 - AI 분석 (2p)
## 섹션 7: AI 분석 (2페이지, 있을 때만) 기존 `ai_pdf.html.erb`의 AI 분석을 메인 PDF에 통합. ### 조건 - `@session.json_data.dig('ai_insights', locale)` 존재할 때만 렌더링 - AI 분석이 없으면 이 섹션 전체 스킵 ### Page A: - comprehensive_insight (강점 종합 분석) - interest_insight (관심 영역 인사이트) ### Page B: - development_plan (강점 개발 방법) - encouragement (응원 메시지) ### 디자인 원칙 - 마크다운 → HTML 변환 (simple_format 또는 커스텀 renderer) - 텍스트 중심, 긴 글 읽기 편한 레이아웃 - 섹션 구분은 제목 + 구분선으로 ### 완료 기준 - 새 파트셜 `_ai_analysis.html.erb` 생성 - pdf.html.erb 오케스트레이터에서 조건부 렌더링 - 기존 ai_pdf.html.erb는 별도 유지 (download_ai_pdf 라우트)
개인의 결제 크레딧, 결제
프로필 메뉴에 내 정보 / 결제 / 크레딧 http://localhost:3000/ko/profile 이런 메뉴 만들어줘. 결제에는 영수증 발급 및 취소도 가능해야해. 취소는 상품별로 달라. 일단 리포트 결제는 취소 불가
멤버 승인 상태 표시 및 i18n
## 목표 이메일 초대 목록에서 해당 멤버의 수락(승인) 여부를 표시 ## 구현 내용 1. WorkspaceInvitation 모델에 승인 상태 확인 헬퍼 추가 - accepted? 메서드: target_email의 사용자가 해당 워크스페이스의 멤버인지 확인 - accepted_member 메서드: 수락한 멤버 객체 반환 - 로직: User.find_by(email: target_email) → workspace.workspace_members.exists?(user: user) 2. _invitation.html.erb 수정 - 승인 상태 배지 추가 - "Pending" (노란색 배지): 아직 수락하지 않음 - "Accepted" (초록색 배지): 멤버로 수락됨 - 수락한 경우 사용자 이름도 표시 - 상태에 따른 아이콘 표시 (체크마크 / 시계) 3. i18n 키 추가 (모든 로케일 파일) - config/locales/en.yml - config/locales/ko.yml - config/locales/zh.yml - config/locales/vi.yml 추가할 키: - invitations.status.pending: "대기 중" / "Pending" / "待处理" / "Đang chờ" - invitations.status.accepted: "수락됨" / "Accepted" / "已接受" / "Đã chấp nhận" - invitations.resend: "재발송" / "Resend" / "重新发送" / "Gửi lại" - invitations.bulk_invite: "벌크 초대" / "Bulk Invite" / "批量邀请" / "Mời hàng loạt" - invitations.bulk_placeholder: 적절한 안내 텍스트 - invitations.bulk_submit: "벌크 발송" / "Send Bulk" / "批量发送" / "Gửi hàng loạt" - invitations.flash.bulk_result: "N건 성공, M건 실패" - invitations.flash.resent: "초대 이메일을 재발송했습니다" 4. 테스트 작성 - test/models/workspace_invitation_test.rb에 accepted? 테스트 추가 ## 담당 파일 (이 파일만 수정) - MODIFY: app/models/workspace_invitation.rb - MODIFY: app/views/workspaces/invitations/_invitation.html.erb - MODIFY: config/locales/en.yml - MODIFY: config/locales/ko.yml - MODIFY: config/locales/zh.yml - MODIFY: config/locales/vi.yml - MODIFY: test/models/workspace_invitation_test.rb ## 완료 기준 - 이메일 초대 목록에서 각 초대의 수락 상태가 표시됨 - Pending/Accepted 배지가 올바르게 표시됨 - 4개 언어 i18n 키 추가됨 - 테스트 통과
k6 부하 테스트 스크립트 작성
## 목적 최적화 효과를 실측으로 검증할 부하 테스트 도구 도입. ## 변경 - `test/load/diagnosis_flow.js` (신규, k6 스크립트) ## 시나리오 1. 진단 세션 생성 → show 페이지 로드 → 45라운드 응답 → complete 2. 동시 사용자: 10 → 50 → 100 → 500 ramp-up 3. 목표 메트릭: p95 < 1s, 에러율 < 1% ## 완료 기준 - [ ] k6 스크립트 작동 - [ ] 스테이징에서 테스트 실행 가능 - [ ] before/after 성능 비교 가능 - [ ] p50, p95, p99, 에러율 메트릭 정의
멤버 결과 보기 권한 통합 테스트 작성
## 목표 워크스페이스 멤버 결과 보기 권한이 올바르게 동작하는지 통합 테스트를 작성한다. ## 테스트 케이스 - [ ] 워크스페이스 admin이 멤버 진단 결과를 볼 수 있는지 - [ ] 워크스페이스 leader가 타인 결과에 접근하면 거부되는지 - [ ] 워크스페이스 member가 타인 결과에 접근하면 거부되는지 - [ ] 워크스페이스 비멤버가 결과에 접근하면 거부되는지 - [ ] 자기 자신의 결과는 역할 무관하게 볼 수 있는지 ## 완료 기준 - [ ] 모든 테스트 통과 - [ ] 기존 테스트 깨지지 않음
표지 + 해석가이드 + 요약 파트셜 리팩토링
3개 1페이지 섹션 파트셜 리팩토링. _cover.html.erb, _way_map_guide.html.erb, _executive_summary.html.erb. 박스/컬러 최소화, 전문적 타이포그래피 중심.
상품 관리
https://9way.org/admin/workspaces 지금은 상품이 강점 진단 리포트 밖에 없지만, 상품관리 메뉴가 필요할 것 같아. 그래야 나중에 안 헷갈릴 듯
Admin BaseController switch_locale override 및 SurveyQuestions 필터링 수정
## 작업 내용 1. `app/controllers/admin/base_controller.rb`에 `switch_locale` override 추가 - Admin 영역에서는 params[:locale]을 무시하고 항상 기본 locale(ko) 사용 - `def switch_locale(&action); I18n.with_locale(I18n.default_locale, &action); end` 2. `app/controllers/admin/survey_questions_controller.rb`의 `group_detail` 액션 수정 - `params[:locale]` 대신 `params[:lang]` 사용 (URL 파라미터명 변경) - `@locale = params[:lang].presence || "ko"` - 선택된 locale에 맞는 질문 번역만 표시되도록 필터링 로직 추가 3. 관련 컨트롤러 테스트 작성 ## 완료 기준 - Admin에서 어떤 파라미터를 전달해도 UI 언어가 변경되지 않음 - group_detail에서 선택된 언어의 질문 텍스트가 정확히 표시됨 - 테스트 통과
개인 PDF - 9WAY MAP 원형 차트 SVG 삽입
## 목표 PDF에 9WAY 원형 차트를 SVG로 직접 삽입하는 새 페이지 추가 ## 작업 내용 - ERB 파트셜 `pdf/_way_map_chart.html.erb` 생성 - 기존 `circular_chart_controller.js` 로직을 서버 사이드 SVG로 변환 - 9 WAY 점수를 방사형(원형) 차트로 시각화 - Domain 섹터 색상 적용 (Thinking: #3B82F6, Relationship: #10B981, Execution: #F59E0B) - Grover(Puppeteer)가 HTML을 렌더링하므로 SVG 직접 임베드 가능 ## 레거시 참고 - `/mnt/c/dev/9way/components/diagnosis/way-map-chart.tsx` - 원형 차트 컴포넌트 - `/mnt/c/dev/9way/lib/pdf/templates/diagnosis/result.tsx` lines 109-146 - 차트 이미지 삽입부 ## 완료 기준 - PDF에 원형 차트가 정상 렌더링됨 - 9개 WAY 점수가 방사형으로 표시됨 - Domain별 색상 구분 적용됨 - 기존 PDF 페이지 번호 조정됨
DNA 차별성 Top5 파트셜 리디자인 (10p)
_dna_detail.html.erb 리디자인. Top5 각 2페이지. detail-box → 구분선+텍스트, green/red 박스 → 텍스트 컬러 힌트. 전문 리포트 느낌.
문항 관리 뷰 언어 선택 파라미터 수정
## 작업 내용 1. `app/views/admin/survey_questions/index.html.erb` 수정 - 언어 버튼 링크에서 `locale:` 파라미터를 `lang:` 으로 변경 - `group_detail_admin_survey_questions_path(diagnosis_type_id: dt.id, lang: locale)` 형태 2. `app/views/admin/survey_questions/group_detail.html.erb` 수정 - 같은 방식으로 `locale:` → `lang:` 변경 - 현재 선택된 언어 표시(active 상태) 확인 3. 통합 테스트 작성 - 언어 버튼 클릭 시 UI 언어가 변경되지 않는지 확인 - 선택된 언어의 질문이 정확히 표시되는지 확인 ## 완료 기준 - 모든 뷰에서 `locale:` 대신 `lang:` 파라미터 사용 - 언어 버튼 클릭 시 전체 UI 언어가 변경되지 않음 - 테스트 통과
개인 PDF - 차트 해석 가이드 텍스트 페이지
## 목표 9WAY MAP 차트와 함께 해석 가이드 텍스트를 PDF 페이지로 추가 ## 작업 내용 - PDF 파트셜 `pdf/_way_map_guide.html.erb` 생성 - 레거시의 5단락 차트 해석 가이드 텍스트를 i18n화 (ko/en/zh/vi) - 원형 차트 SVG와 가이드 텍스트를 한 페이지 또는 연속 페이지로 배치 - `pdf.html.erb` 오케스트레이터에 새 페이지 삽입 (P3 위치) ## 레거시 참고 (가이드 텍스트 5단락) 1. 강점을 제대로 이해하려면 3가지 핵심 질문... (영역/역할/차별성) 2. 아래 그래프에서 사고, 관계, 행동 세 영역 중... (Domain 해석) 3. 원 주변 부채꼴 면적인 9가지 강점 역할... (WAY 해석) 4. 강점 역할 바깥쪽 DNA 아이콘... (DNA 해석) 5. 다음 페이지들을 천천히 읽어보세요... (활용 안내) 참고 파일: `/mnt/c/dev/9way/lib/pdf/templates/diagnosis/result.tsx` lines 112-138 ## 완료 기준 - 가이드 텍스트가 PDF에 정상 표시됨 - 4개 언어 i18n 적용됨 - 차트와 가이드가 자연스럽게 배치됨
강점 프로파일 + 탁월한 역할 + AI 분석 파트셜 신규 생성
3개 신규 파트셜 생성. _strength_profile.html.erb(domain+way 분포 2p), _excellent_role.html.erb(WAY×DNA 조합+target별 콘텐츠 2p), _ai_analysis.html.erb(AI 인사이트 2p 조건부).
CustomPaymentLink 모델 + 마이그레이션 + 라우팅 + 서비스
## 작업 내용 CustomPaymentLink 모델, DB 마이그레이션, 라우팅, 서비스 객체를 생성합니다. ## 1. DB 마이그레이션 ```ruby create_table :custom_payment_links, id: :uuid do |t| t.string :title, null: false # 상품명 t.integer :amount, null: false # 결제 금액 (원) t.text :description # 상세 설명 t.string :status, default: "pending", null: false # pending/completed/canceled t.string :token, null: false # URL 토큰 t.datetime :expires_at, null: false # 만료일 t.references :created_by, type: :uuid, null: false, foreign_key: { to_table: :users } t.references :payment, type: :uuid, foreign_key: true # 결제 완료 시 연결 t.datetime :paid_at # 결제 완료 시각 t.timestamps end add_index :custom_payment_links, :token, unique: true add_index :custom_payment_links, :status ``` ## 2. CustomPaymentLink 모델 - belongs_to :created_by, class_name: "User" - belongs_to :payment, optional: true - enum :status (pending, completed, canceled) - validates :title, :amount, :token, :expires_at presence - validates :amount, numericality: { greater_than: 0 } - validates :token, uniqueness: true - before_validation :generate_token (SecureRandom.urlsafe_base64(32)) - scope :active → pending + 만료 안 됨 - scope :recent → created_at desc - expired? 메서드 - complete!(payment:) 메서드 - cancel! 메서드 ## 3. 라우팅 (config/routes.rb) admin namespace 안에 추가: ```ruby resources :custom_payment_links, only: %i[index new create] do member do patch :cancel get :qr_code end end ``` locale scope 밖에 추가 (토큰 기반 공개 접근): ```ruby # Custom Payment Links (토큰 기반 공개 결제) get "pay/:token", to: "custom_payments#show", as: :custom_payment get "pay/:token/success", to: "custom_payments#success", as: :custom_payment_success get "pay/:token/fail", to: "custom_payments#fail", as: :custom_payment_fail ``` ## 4. 서비스 객체 ### CustomPaymentLinks::CreateService - 입력: title, amount, description, expires_in(일수), created_by(User) - expires_at = expires_in.days.from_now - CustomPaymentLink.create! - 반환: custom_payment_link ### CustomPaymentLinks::CancelService - 입력: custom_payment_link - pending? 검증 - update!(status: "canceled") ## 5. 테스트 - 모델 테스트: 유효성 검증, 상태 전환, scope - 서비스 테스트: 생성, 취소 시나리오 ## 담당 파일 - db/migrate/XXXX_create_custom_payment_links.rb - app/models/custom_payment_link.rb - config/routes.rb - app/services/custom_payment_links/create_service.rb - app/services/custom_payment_links/cancel_service.rb - test/models/custom_payment_link_test.rb - test/fixtures/custom_payment_links.yml ## 완료 기준 - 마이그레이션 실행 성공 - 모델 테스트 통과 - 서비스 테스트 통과 - routes 정상 작동
개인 PDF - WAY 상세 진단 타입별 8개 섹션
## 목표 WAY 상세 페이지를 진단 타입별 8개 구조화 섹션으로 재구성 ## 작업 내용 - `pdf/_way_detail.html.erb` 리팩토링 - 타입별 섹션 제목 매핑: - **Adult/College**: 강점 슬로건, 탁월한 역할, 나만의 차별성, 주의할 모습, 스트레스 상황, 자주하는 말, 커리어 TIP, 성장 가이드 - **Student**: 강점 슬로건, 학습 스타일, 나만의 차별성, 효과적인 공부 방법, 집중력이 높아지는 환경, 집중력이 떨어지는 환경, 진로 TIP, 성장 가이드 - **Parent**: 강점 슬로건, 학습 스타일, 자녀의 차별성, 자녀를 대할 때 주의할 점, 스트레스 상황, 듣기 싫은 말, 진로 TIP, 코칭 가이드 - **Leader**: 강점 슬로건, 리더십 스타일, 강점의 차별성, 리더십 강점, 리더십 함정, 성숙을 위한 과제, 갈등 유발 대상, 리더십 TIP - ResultFormatter에 WAY detailedDescription 데이터 매핑 확인/추가 - StrengthDescription에서 WAY별 상세 설명 조회 (field_type 확인) - 1위 WAY만 표시 (레거시 동일) ## 레거시 참고 - `/mnt/c/dev/9way/lib/pdf/templates/diagnosis/result.tsx` lines 212-306 - WAY 상세 섹션 - `sectionTitlesByType` 객체 (lines 218-269) - 타입별 섹션 제목 정의 ## 완료 기준 - 5가지 진단 타입별 적절한 섹션 제목이 표시됨 - 1위 WAY의 8개 섹션이 구조화되어 렌더링됨 - 기존 summary_contents + 27 DNA 랭킹은 별도 페이지로 유지
관리자 커스텀 결제 링크 UI (컨트롤러 + 뷰 + 사이드바)
## 작업 내용 관리자가 커스텀 결제 링크를 생성하고 관리할 수 있는 Admin UI를 구현합니다. ## 1. Admin::CustomPaymentLinksController - BaseController 상속 (Admin::BaseController) - index: 결제 링크 목록 (최신순, 페이지네이션 없이 전체) - new: 생성 폼 - create: CustomPaymentLinks::CreateService 호출 - cancel: CustomPaymentLinks::CancelService 호출 - qr_code: QR 코드 SVG 반환 ## 2. 뷰 (Admin Views) ### index.html.erb - 결제 링크 목록 참고 이미지의 UI를 구현: - 헤더: "생성된 결제 링크 목록", "총 N개의 커스텀 결제 링크" - 테이블 컬럼: 상품명, 금액, 상태, 생성일, 만료일, 작업 - 상태 배지: - 대기중 (pending): 회색 배경, "⏳ 대기중" - 결제완료 (completed): 보라색 배경, "✅ 결제완료" - 취소됨 (canceled): 회색 텍스트, "⊘ 취소됨" - 작업 컬럼: - 대기중: "복사" 버튼 + "취소" 버튼 (빨간색) - 결제완료: "결제완료: YYYY.MM.DD" 텍스트 - 취소됨: 작업 없음 - "새 결제 링크 만들기" 버튼 (상단) - 금액은 number_to_currency(amount, unit: "₩", precision: 0) 형식 ### new.html.erb - 결제 링크 생성 폼 참고 이미지의 UI를 구현: - 제목: "커스텀 결제 링크 생성" - 설명: "관리자가 임의의 금액으로 결제 링크를 생성할 수 있습니다. 생성된 링크는 고객에게 공유하여 결제를 받을 수 있습니다." - 폼 필드: - 상품명 * (text_field, placeholder: "예: 1:1 코칭 컨설팅") - 결제 금액 (원) * (number_field, placeholder: "예: 150000") - 상세 설명 (선택) (text_area, placeholder: "예: 2시간 1:1 강점 코칭 세션") - 유효 기간 (select: 7일/14일/30일) - "결제 링크 생성" 버튼 (보라색, 전체 너비) ### QR 코드 기능 - rqrcode gem 사용 (이미 Gemfile에 있음) - 링크 목록에서 "QR" 아이콘 클릭 시 모달/팝오버로 QR 표시 - Stimulus controller로 모달 토글 ### 링크 복사 기능 - "복사" 버튼 클릭 시 클립보드에 결제 링크 URL 복사 - navigator.clipboard.writeText() 사용 - Stimulus controller: clipboard_controller.js ## 3. 사이드바 수정 app/views/admin/shared/_sidebar.html.erb에 추가: - workspaces 메뉴 아래에 추가 - icon: "payments" (결제 아이콘 SVG path 추가) - label: "커스텀 결제" - active: controller_name == "custom_payment_links" - _nav_item.html.erb의 icons 해시에 "payments" 아이콘 추가 ## 4. i18n config/locales/ko.yml에 추가: ```yaml admin: nav: custom_payments: "커스텀 결제" custom_payment_links: title: "커스텀 결제 링크" subtitle: "총 %{count}개의 커스텀 결제 링크" new_link: "새 결제 링크 만들기" create_title: "커스텀 결제 링크 생성" create_description: "관리자가 임의의 금액으로 결제 링크를 생성할 수 있습니다..." form: title: "상품명" title_placeholder: "예: 1:1 코칭 컨설팅" amount: "결제 금액 (원)" amount_placeholder: "예: 150000" description: "상세 설명 (선택)" description_placeholder: "예: 2시간 1:1 강점 코칭 세션" expires_in: "유효 기간" submit: "결제 링크 생성" status: pending: "대기중" completed: "결제완료" canceled: "취소됨" actions: copy: "복사" cancel: "취소" qr: "QR" flash: created: "결제 링크가 생성되었습니다." canceled: "결제 링크가 취소되었습니다." copied: "링크가 클립보드에 복사되었습니다." ``` ## 5. 기존 UI 패턴 참고 - 테이블: bg-white rounded-xl shadow-sm border, thead bg-surface-emphasis, tr hover:bg-surface-emphasis - 배지: inline-flex items-center px-2 py-0.5 rounded text-xs font-medium - 버튼: px-4 py-2 bg-admin-primary text-white rounded-lg - 폼: w-full rounded-lg border-border-default focus:border-admin-primary ## 담당 파일 - app/controllers/admin/custom_payment_links_controller.rb - app/views/admin/custom_payment_links/index.html.erb - app/views/admin/custom_payment_links/new.html.erb - app/views/admin/shared/_sidebar.html.erb (수정) - app/views/admin/shared/_nav_item.html.erb (아이콘 추가) - config/locales/ko.yml (admin 섹션 수정) - app/javascript/controllers/clipboard_controller.js (새 파일) - app/javascript/controllers/qr_modal_controller.js (새 파일) - test/controllers/admin/custom_payment_links_controller_test.rb ## 완료 기준 - Admin 결제 링크 목록 페이지 작동 - 생성 폼 작동 (유효성 검증 포함) - 취소 기능 작동 - 복사 기능 작동 - QR 코드 표시 - 사이드바 메뉴 표시 - 컨트롤러 테스트 통과
개인 PDF - Domain 상세 페이지 개선
## 목표 Domain 상세 페이지를 1위 도메인 중심 + 3영역 비율 바차트로 개선 ## 작업 내용 - `pdf/_domain_detail.html.erb` 수정 - 1위 Domain 중심 레이아웃으로 변경 (현재는 3개 모두 동등 표시) - 3영역 비율 바 차트 추가 (Thinking/Relationship/Execution 비율 표시) - 1위 Domain의 상세 설명 문단 추가 (현재는 description만 있고 detailedDescription 없음) - 비율 표시: 실제 비율 % + 정규화된 바 너비 ## 레거시 참고 - `/mnt/c/dev/9way/lib/pdf/templates/diagnosis/result.tsx` lines 148-210 - 3영역 비율 계산: totalScore 대비 각 domain score 비율 - 50%를 최대값으로 정규화하여 바 차트 표시 - 색상: Thinking #3b82f6, Relationship #10b981, Execution #f97316 ## 완료 기준 - 1위 Domain이 강조되어 표시됨 - 3영역 비율 바 차트가 렌더링됨 - 상세 설명 문단이 포함됨
고객 결제 페이지 + Toss Payments 연동
## 작업 내용 고객이 커스텀 결제 링크를 통해 접근하여 Toss Payments로 결제할 수 있는 공개 페이지를 구현합니다. ## 1. CustomPaymentsController 로그인 불요 (공개 페이지). ApplicationController 상속하되 skip_before_action :require_authentication. ### show (GET /pay/:token) - CustomPaymentLink.find_by!(token: params[:token]) - 만료/취소/완료 여부 체크 - 만료됨 → expired 상태 표시 - 취소됨 → canceled 상태 표시 - 결제완료 → 이미 완료 메시지 - 대기중 → 결제 폼 표시 ### checkout (POST /pay/:token/checkout) - 결제 링크 유효성 검증 (pending? && !expired?) - 고객용 임시 User 생성 또는 guest 처리 - 실제로는 Toss에서 결제 처리하므로 user 없이도 가능 - Payment 생성 시 user_id는 결제 링크 생성자(admin)의 ID 사용 - Payments::CheckoutService 호출: - payment_type: "CUSTOM" - amount: custom_payment_link.amount - description: custom_payment_link.title - metadata: { custom_payment_link_id: link.id } - JSON 응답으로 Toss SDK용 데이터 반환 ### success (GET /pay/:token/success) - Payments::ConfirmationService로 결제 승인 - CustomPaymentLink 상태를 completed로 변경 - payment 연결, paid_at 설정 - 성공 페이지 표시 ### fail (GET /pay/:token/fail) - 실패 메시지 표시 - 다시 시도 링크 제공 ## 2. 뷰 ### show.html.erb - 결제 페이지 - 독립 레이아웃 (admin/user 레이아웃 아님) - 깔끔한 결제 카드 형태: - 9WAY 로고 - 상품명 (title) - 상세 설명 (description) - 결제 금액 (amount) - 큰 글씨 - "결제하기" 버튼 - Toss Payments SDK 로드: ```html <script src="https://js.tosspayments.com/v2/standard"></script> ``` - Stimulus 또는 vanilla JS로 Toss 결제 위젯 초기화 - 결제 시퀀스: 1. "결제하기" 클릭 2. POST /pay/:token/checkout으로 주문 생성 3. 응답의 orderId, amount로 Toss requestPayment() 호출 4. 성공 → /pay/:token/success?paymentKey=...&orderId=...&amount=... 5. 실패 → /pay/:token/fail?code=...&message=... - 만료/취소/완료 시 적절한 메시지 표시 ### success.html.erb - 결제 성공 - 성공 아이콘 + "결제가 완료되었습니다" 메시지 - 결제 정보 요약 (상품명, 금액, 결제일시) ### 기존 참고 코드 - app/controllers/payments_controller.rb의 checkout/success/fail 패턴 - app/services/payments/checkout_service.rb의 결제 생성 로직 - app/services/payments/confirmation_service.rb의 결제 승인 로직 - 기존 Toss client_key: Rails.application.config.toss_payments.client_key ## 3. 레이아웃 - 별도의 minimal 레이아웃 사용: layout "custom_payment" - app/views/layouts/custom_payment.html.erb 생성 - 심플한 흰색 배경 + 가운데 정렬 카드 ## 4. 라우팅 라우팅은 "백엔드" 에이전트가 처리합니다. 다음 경로를 사용합니다: - GET /pay/:token → custom_payments#show - POST /pay/:token/checkout → custom_payments#checkout - GET /pay/:token/success → custom_payments#success - GET /pay/:token/fail → custom_payments#fail ## 담당 파일 - app/controllers/custom_payments_controller.rb - app/views/custom_payments/show.html.erb - app/views/custom_payments/success.html.erb - app/views/layouts/custom_payment.html.erb - app/javascript/controllers/toss_custom_payment_controller.js - test/controllers/custom_payments_controller_test.rb ## 완료 기준 - 유효한 토큰으로 결제 페이지 접근 가능 - 만료/취소/완료 상태 적절히 표시 - Toss Payments 결제 플로우 동작 - 결제 성공 시 CustomPaymentLink 상태 업데이트 - 컨트롤러 테스트 통과
개인 PDF - 유료 콘텐츠 페이지 구현
## 목표 PDF에 유료 콘텐츠 페이지 추가 (report_purchased? 조건부 렌더링) ## 작업 내용 - PDF 파트셜 `pdf/_premium_content.html.erb` 생성 - 직무별 상세 페이지 (각 직무마다 개별 페이지): - 하는 일, 주요 과업, 직업 트렌드, 필요 지식, 필요 스킬, 필요 경험, 준비할 것 - 자소서/이력서 TIP 페이지 (bullet 리스트) - 진로/커리어 가이드 페이지 - `pdf.html.erb` 오케스트레이터에서 `session.report_purchased?` 조건부 렌더링 - ResultFormatter에 premium_content 데이터 매핑 추가 - StrengthDescription에서 job/course/study 등 summary field_type 활용 ## 레거시 참고 - `/mnt/c/dev/9way/lib/pdf/templates/diagnosis/result.tsx` lines 382-490 - 유료 콘텐츠 렌더링 - `/mnt/c/dev/9way/lib/pdf/templates/diagnosis/types.ts` lines 86-102 - PremiumSection/PremiumContentItem 타입 - 직무별: jobDescriptions Record (직무명 → 설명) - 일반: contents string[] (자소서 tip, 학교생활 tip 등) ## 완료 기준 - report_purchased?인 경우에만 유료 페이지가 PDF에 포함됨 - 직무별 상세가 개별 페이지로 렌더링됨 - 자소서TIP/진로가이드가 정상 표시됨 - 미결제 시 유료 페이지가 PDF에 포함되지 않음
인프라 설정 + 통계 컨트롤러 + 개요 대시보드
Chartkick gem + Chart.js importmap 설정, Admin::StatisticsController 생성 (show/users/diagnoses/revenue 4개 액션 + 데이터 쿼리), routes.rb에 statistics 라우트 추가, 사이드바에 통계 메뉴 추가, 개요 대시보드 뷰(show.html.erb) + 탭 네비게이션 생성. 선행 작업으로 다른 서브 티켓의 의존성.
SVG 원형 차트 헬퍼 + PDF 파트셜 생성
circular_chart_controller.js 로직을 분석하여 서버 사이드 SVG 원형 차트를 생성하는 헬퍼 메서드를 만들고, PDF 파트셜 _way_map_chart.html.erb를 생성한다. 작업 범위: 1. app/helpers/pdf_chart_helper.rb에 way_map_svg 메서드 추가 2. app/views/diagnoses/pdf/_way_map_chart.html.erb 파트셜 생성 3. 9 WAY 점수를 방사형 차트로 시각화 4. Domain 섹터 색상 적용 (Thinking: #3B82F6, Relationship: #10B981, Execution: #F59E0B) 참고 파일: - app/javascript/controllers/circular_chart_controller.js (현재 JS 차트 로직) - app/helpers/chart_helper.rb (색상 정의, WAY_DOMAIN_MAP 등) - /mnt/c/dev/9way/components/diagnosis/way-map-chart.tsx (레거시 차트)
사용자 + 진단 분석 뷰
users.html.erb (사용자 가입 추이 차트, 역할 분포, 월간 활성 사용자) + diagnoses.html.erb (진단 완료 추이, 유형별 분포, 상태별 통계, 평균 완료 시간) 뷰 생성. StatisticsController의 인스턴스 변수 활용. 의존: 인프라 설정 서브 티켓 완료 후 시작.
가이드 텍스트 i18n + PDF 파트셜 + 오케스트레이터 수정
차트 해석 가이드 텍스트를 i18n에 추가하고, PDF 파트셜을 생성하고, PDF 오케스트레이터를 수정하여 새 페이지를 삽입한다. 작업 범위: 1. config/locales/ko.yml에 가이드 텍스트 5단락 추가 (diagnosis.pdf.chart_guide 키) 2. app/views/diagnoses/pdf/_way_map_guide.html.erb 파트셜 생성 3. app/views/diagnoses/pdf.html.erb 오케스트레이터 수정 (P3: guide+chart 페이지 삽입, 이후 페이지 번호 조정) 4. PDF CSS에 가이드 텍스트 스타일 추가 가이드 텍스트 원문 (레거시): 1. "강점을 제대로 이해하려면 3가지 핵심 질문에 답할 수 있어야 합니다..." 2. "먼저 아래 그래프에서 사고, 관계, 행동 세 영역 중..." 3. "다음으로 원 주변에 있는 부채꼴 면적인, 9가지 강점 역할을..." 4. "마지막으로 강점 역할 바깥쪽에 위치한 DNA 아이콘을..." 5. "이제 다음 페이지들을 천천히 읽어보세요..." 참고 파일: - /mnt/c/dev/9way/lib/pdf/templates/diagnosis/result.tsx lines 112-138 - app/views/diagnoses/pdf.html.erb (현재 오케스트레이터) - config/locales/ko.yml (현재 i18n)
매출 분석 뷰 + 통합 테스트
revenue.html.erb (매출 추이 차트, 결제 유형 분포, 워크스페이스 크레딧 사용량) 뷰 생성 + Admin::StatisticsController 통합 테스트 작성. StatisticsController의 인스턴스 변수 활용. 의존: 인프라 설정 서브 티켓 완료 후 시작.
프로필 탭 네비게이션 + 라우트 + i18n
## 목표 프로필 페이지에 탭 네비게이션(내 정보/결제/크레딧)을 구현하고, 라우트와 i18n을 설정합니다. ## 완료 기준 1. `app/views/shared/_profile_tabs.html.erb` 탭 partial 생성 - 3개 탭: 내 정보(profile_path), 결제(payments_path), 크레딧(credits_path) - 현재 페이지 활성 탭 표시 (Tailwind CSS) - 모바일/데스크탑 반응형 2. `app/views/profiles/show.html.erb` 수정 - 기존 프로필 정보 위에 탭 네비게이션 추가 - 기존 UI 유지하면서 탭 통합 3. `config/routes.rb` 수정 - `resources :credits, only: [:index]` 추가 (locale scope 내) 4. i18n 키 추가 (4개 언어: ko, en, zh, vi) - `config/locales/ko.yml`: profile.tabs.info, profile.tabs.payments, profile.tabs.credits - `config/locales/en.yml`, `zh.yml`, `vi.yml` 동일 구조 5. 통합 테스트 작성 ## 담당 파일 (이 파일만 수정) - `app/views/shared/_profile_tabs.html.erb` (신규) - `app/views/profiles/show.html.erb` (수정) - `config/routes.rb` (수정) - `config/locales/ko.yml` (수정) - `config/locales/en.yml` (수정) - `config/locales/zh.yml` (수정) - `config/locales/vi.yml` (수정) - `test/integration/profile_tabs_test.rb` (신규)
Admin Products 라우트 + 컨트롤러 구현
Admin::ProductsController CRUD 구현 ## 수정 파일 - config/routes.rb: admin 네임스페이스에 resources :products 추가 (toggle_active member 라우트 포함) - app/controllers/admin/products_controller.rb: 새 파일 생성 ## 컨트롤러 요구사항 - Admin::BaseController 상속 (require_admin 자동 적용) - 액션: index, show, new, create, edit, update, toggle_active - index: 검색(q param, name ILIKE), 필터(product_type, active), 정렬(sort/direction), 페이지네이션(PER_PAGE=30) - show: @product with product_prices eager load - create/update: strong params (name, slug, description, price, currency, product_type, active, product_prices_attributes[id, currency, amount, _destroy]) - toggle_active: active 토글 후 redirect - accepts_nested_attributes_for :product_prices (모델에 추가 필요) - flash 메시지: notice/alert, i18n 키 사용 - 기존 Admin::WorkspacesController 패턴 참고 ## 완료 기준 - rails routes | grep admin/products 로 라우트 확인 - 컨트롤러 파일이 기존 패턴과 일관성 있음
개인 크레딧 페이지 생성
## 목표 개인 크레딧 잔액 및 거래 내역을 보여주는 페이지를 생성합니다. ## 완료 기준 1. `app/controllers/credits_controller.rb` 생성 - layout "authenticated" - index 액션: Current.user의 크레딧 잔액 + CreditTransaction 내역 (최신순) - require_authentication 적용 2. `app/views/credits/index.html.erb` 생성 - 상단: `shared/_profile_tabs` partial 렌더 (탭 네비게이션) - 크레딧 잔액 카드 (큰 숫자로 표시) - 거래 내역 테이블: 날짜, 유형(purchase/usage/admin_grant/refund), 설명, 변동량(+/-), 잔액 - 변동량: 양수는 초록, 음수는 빨강 - 빈 상태 처리 (거래 내역 없을 때) - Tailwind CSS, 기존 shared/_card partial 활용 3. 통합 테스트 작성 - 로그인 후 크레딧 페이지 접근 가능 - 크레딧 잔액 표시 확인 - 거래 내역 표시 확인 ## 참고: 기존 모델 - UserCredit: user_id(unique), balance(integer) - CreditTransaction: user_id, amount, balance_after, transaction_type, description, payment_id, diagnosis_session_id - User#credit_balance: credit&.balance || 0 - User#ensure_credit!: credit || create_credit!(balance: 0) ## 담당 파일 (이 파일만 수정/생성) - `app/controllers/credits_controller.rb` (신규) - `app/views/credits/index.html.erb` (신규) - `test/controllers/credits_controller_test.rb` (신규) ## 주의사항 - `shared/_profile_tabs` partial은 agent-nav가 생성합니다. 해당 파일이 아직 없으면, 탭 partial 렌더링 부분을 주석 처리하고 나중에 통합하세요. - routes.rb는 agent-nav가 수정합니다. credits 라우트가 이미 추가되어 있을 것입니다. - i18n 키는 agent-nav가 관리합니다. t() 호출은 사용하되, locales 파일은 직접 수정하지 마세요.
Admin Products 뷰 + 사이드바 + i18n 구현
Admin Products 뷰 파일, 사이드바 메뉴, i18n 번역 구현 ## 생성할 파일 - app/views/admin/products/index.html.erb - app/views/admin/products/show.html.erb - app/views/admin/products/new.html.erb - app/views/admin/products/edit.html.erb - app/views/admin/products/_form.html.erb ## 수정할 파일 - app/views/admin/shared/_sidebar.html.erb: products 메뉴 추가 (diagnosis_sessions와 workspaces 사이) - app/views/admin/shared/_nav_item.html.erb: products 아이콘 SVG 추가 (쇼핑백/상자 아이콘) - config/locales/ko.yml: admin.nav.products, admin.products.* 번역 키 추가 - config/locales/en.yml: 영문 번역 추가 ## 뷰 요구사항 ### index.html.erb - 기존 admin/workspaces/index.html.erb 패턴 참고 - 검색바 (name 검색) - 필터: product_type (report, credit_pack), active (true/false/all) - 테이블: slug, name, product_type, price, currency, active 상태 배지, 등록일 - 각 행에 show/edit 링크 - 페이지네이션 (shared/_pagination 파티션 사용) - 새 상품 등록 버튼 ### show.html.erb - 상품 기본 정보 카드 (name, slug, description, product_type, price, currency, active, metadata) - 다통화 가격 목록 (ProductPrice 테이블) - 편집/목록 버튼 ### _form.html.erb (new/edit 공유) - 필드: name, slug, description, price, currency (select), product_type (select), active (checkbox) - ProductPrice nested form: currency + amount 행 추가/삭제 가능 (Stimulus 없이 순수 HTML로) - 기존 shared 파티션(shared/_input, shared/_button, shared/_card) 활용 ### 사이드바 - icon key: "products" - i18n key: admin.nav.products - path: admin_products_path - active: controller_name == "products" ### i18n 키 (ko.yml) - admin.nav.products: "상품" - admin.products.title: "상품 관리" - admin.products.new_product: "새 상품 등록" - admin.products.edit_product: "상품 수정" - admin.products.form.*: 폼 라벨들 - admin.products.show.*: 상세 화면 라벨들 ## Tailwind CSS - 기존 admin 뷰와 동일한 디자인 시스템 사용 - 반응형 테이블, 상태 배지, 카드 레이아웃 ## 완료 기준 - 사이드바에 상품 메뉴가 표시됨 - 모든 뷰 파일이 기존 admin 패턴과 일관됨 - i18n 키가 누락 없이 등록됨
결제 내역 UI 개선 + 취소 정책
## 목표 결제 내역 페이지에 프로필 탭을 적용하고, 영수증 링크를 강화하며, 리포트 결제 취소 불가 정책을 구현합니다. ## 완료 기준 ### 1. 결제 내역 UI 개선 - `app/views/payments/index.html.erb` 수정 - 상단에 `shared/_profile_tabs` partial 렌더 (탭 네비게이션) - 각 결제 항목에 영수증 링크 표시 (receipt_url이 있으면) - 취소 불가 상품은 "취소불가" 배지 표시 - `app/views/payments/show.html.erb` 수정 - 상단에 `shared/_profile_tabs` partial 렌더 - 리포트 결제일 경우 취소 버튼 대신 "리포트 결제는 취소할 수 없습니다" 안내 ### 2. 리포트 결제 취소 불가 정책 - `app/models/payment.rb` 수정 - `refundable?` 메서드에 `payment_type != "REPORT"` 조건 추가 - 즉: completed && not canceled && 7일 이내 && payment_type != "REPORT" - `app/services/payments/cancellation_service.rb` 수정 (필요시) - 리포트 결제 취소 요청 시 명확한 에러 메시지 ### 3. 테스트 - Payment#refundable? 테스트: 리포트 결제는 false 반환 - CancellationService 테스트: 리포트 결제 취소 거부 - 통합 테스트: 결제 내역 페이지 탭 표시, 리포트 결제 취소 버튼 미표시 ## 참고: 기존 코드 - Payment.payment_type: REPORT, CAREER_UP, PREMIUM, ANALYSIS_CREDIT, PACKAGE, WORKSHOP, STRENGTH_DIARY, CUSTOM - Payment#refundable?: completed? && canceled_at.nil? && approved_at > 7.days.ago - receipt_url 컬럼이 이미 존재 ## 담당 파일 (이 파일만 수정) - `app/views/payments/index.html.erb` (수정) - `app/views/payments/show.html.erb` (수정) - `app/models/payment.rb` (수정) - `app/services/payments/cancellation_service.rb` (수정, 필요시) - `test/models/payment_test.rb` (수정/생성) - `test/services/payments/cancellation_service_test.rb` (수정/생성) - `test/integration/payments_profile_tab_test.rb` (신규) ## 주의사항 - `shared/_profile_tabs` partial은 agent-nav가 생성합니다. 아직 없으면 주석 처리 후 나중에 통합. - i18n 키는 agent-nav가 관리합니다. t() 호출은 사용하되, locales 파일은 직접 수정하지 마세요. - routes.rb는 수정하지 마세요 (agent-nav 담당)
Admin Products 통합 테스트 구현
Admin Products 통합 테스트 작성 ## 생성/수정할 파일 - test/integration/admin/products_test.rb: 새 파일 생성 - test/fixtures/products.yml: 기존 fixtures 확인 및 보완 - test/fixtures/product_prices.yml: 기존 fixtures 확인 및 보완 ## 테스트 요구사항 (Minitest) ### 인증/권한 테스트 - 비로그인 사용자 접근 시 로그인 페이지로 리다이렉트 - 일반 사용자 접근 시 접근 거부 - admin 사용자만 접근 가능 ### index 테스트 - 상품 목록 표시 - name 검색 (q 파라미터) - product_type 필터 - active 필터 - 정렬 (sort/direction 파라미터) ### CRUD 테스트 - new: 새 상품 폼 렌더링 - create: 유효한 파라미터로 상품 생성 - create: 무효한 파라미터(slug 중복 등)로 실패 - show: 상품 상세 + product_prices 표시 - edit: 편집 폼 렌더링 - update: 상품 정보 수정 - update: product_prices 추가/수정/삭제 ### toggle_active 테스트 - 활성 → 비활성 토글 - 비활성 → 활성 토글 ## 테스트 패턴 - 기존 test/integration/admin/workspaces_test.rb 패턴 참고 - sign_in_as(users(:admin_user)) 사용 - assert_response, assert_redirected_to, assert_difference 활용 - fixtures 기반 테스트 데이터 ## 완료 기준 - bin/rails test test/integration/admin/products_test.rb 통과 - 모든 CRUD + 필터 + 권한 테스트 커버