부모 티켓
29개 티켓

백로그

0
티켓 없음

할 일

11
높음 b97ac168
서브 티켓 [조직문화 PR1] DB: WorkspaceEngagementSurvey 모델 + DiagnosisSession 확장

WorkspaceEngagementSurvey 마이그레이션 + 모델 + DiagnosisSession 확장

## 작업 내용 ### 1. 마이그레이션: create_workspace_engagement_surveys ``` - id: uuid, primary_key - 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 (enum: draft=0, active=1, closed=2) - access_token: string, NOT NULL, unique index - started_at: datetime - ended_at: datetime - timestamps - 복합 unique 인덱스: [workspace_id, year, quarter] ``` ### 2. 마이그레이션: add_engagement_survey_to_diagnosis_sessions ``` - workspace_engagement_survey_id: uuid, FK(workspace_engagement_surveys), index (nullable) - workspace_group_id: uuid, FK(workspace_groups), index (nullable) ``` ### 3. WorkspaceEngagementSurvey 모델 - belongs_to :workspace - belongs_to :created_by, class_name: "User" - has_many :diagnosis_sessions (workspace_engagement_survey_id) - enum :status, { draft: 0, active: 1, closed: 2 } - validates: workspace, title, year, quarter, access_token presence - validates: quarter, inclusion: 1..4 - validates: [workspace_id, year, quarter] uniqueness (scope) - before_validation: access_token 자동 생성 (SecureRandom.alphanumeric(8).upcase) - scope :for_workspace → where(workspace_id:) - scope :active → where(status: :active) - scope :by_period → order(year: :desc, quarter: :desc) ### 4. DiagnosisSession 확장 - belongs_to :workspace_engagement_survey, optional: true - belongs_to :workspace_group, class_name: "WorkspaceGroup", optional: true ### 5. Workspace 모델 확장 - has_many :engagement_surveys, class_name: "WorkspaceEngagementSurvey" ### 6. fixture 생성 - test/fixtures/workspace_engagement_surveys.yml (2~3개) ### 7. 테스트 - test/models/workspace_engagement_survey_test.rb - 유효성 검사 (필수 필드, quarter 범위, uniqueness) - 연관관계 - enum - access_token 자동 생성 - 스코프 - DiagnosisSession 연관관계 테스트 ## 중요 사항 - 모든 rails 명령은 `docker compose exec web` 으로 실행 - TDD: 테스트 먼저 작성 → 구현 - DB 안전: test 환경에서만 마이그레이션 테스트

D
developer-1
4 months
높음 614a8fe8
서브 티켓 [조직문화 PR2] 서비스: 설문 생성/응답 제출/결과 집계

EngagementSurveys 서비스 3개 TDD 구현

## 작업 내용: 3개 서비스 객체 구현 ### 참조 스킬 - `.claude/skills/service-objects/SKILL.md` — Result 패턴, 네이밍 컨벤션 - `.claude/skills/rails-testing/SKILL.md` — Minitest 테스트 패턴 ### 기존 코드 참조 (반드시 읽기) - `app/services/diagnosis/scoring/engagement_calculator.rb` — 기존 채점 로직 (FACTOR_DIMENSION_MAP 등) - `app/services/diagnosis/engagement_result_formatter.rb` — 결과 포맷 - `app/services/diagnosis/score_calculator_factory.rb` — 팩토리 - `app/models/workspace_engagement_survey.rb` — PR1에서 생성한 모델 - `app/models/diagnosis_session.rb` — DiagnosisSession 구조 - `db/seeds/engagement/engagement_questions_b2b.csv` — B2B 10문항 구조 ### 1. EngagementSurveys::CreateService 파일: `app/services/engagement_surveys/create_service.rb` ```ruby # 호출: EngagementSurveys::CreateService.call(workspace:, user:, year:, quarter:) # 반환: Result (success/failure) ``` - workspace + year + quarter로 WorkspaceEngagementSurvey 생성 - access_token은 모델에서 자동 생성 - 동일 workspace+year+quarter 중복 시 실패 반환 - title은 자동 생성: "#{year} Q#{quarter}" ### 2. EngagementSurveys::SubmitResponseService 파일: `app/services/engagement_surveys/submit_response_service.rb` ```ruby # 호출: EngagementSurveys::SubmitResponseService.call( # survey:, workspace_group:, responses:, user: nil # ) # responses: [{ question_no: 1, dimension: "belonging", factor: "psychological_safety", value: 5 }, ...] # 반환: Result (success/failure) ``` - DiagnosisSession을 생성하여 응답 저장 - context_type: "b2b" - workspace_engagement_survey_id: survey.id - workspace_group_id: workspace_group.id - user_id: user&.id (로그인 시 참고용, nil 허용) - scoring_framework: "engagement_3factor" - 설문이 active 상태가 아니면 실패 반환 - responses를 DiagnosisSession의 response_data(또는 적절한 필드)에 저장 - 기존 EngagementCalculator로 채점하여 결과 저장 - 주의: 기존 DiagnosisSession이 어떤 필드에 응답/결과를 저장하는지 확인 후 동일 패턴 사용 ### 3. EngagementSurveys::ResultAggregator 파일: `app/services/engagement_surveys/result_aggregator.rb` ```ruby # 호출: EngagementSurveys::ResultAggregator.call(survey:, group: nil) # group이 nil이면 전체 집계, 있으면 해당 그룹만 # 반환: Result with data: # { # total_responses: Integer, # overall_score: Float, # 10번 문항(overall) 전체 평균 # engagement_types: { # 10번 문항 기준 비율 # active: Float, # 6,7점 비율 (%) # passive: Float, # 4,5점 비율 (%) # disengaged: Float # 1,2,3점 비율 (%) # }, # factor_scores: { # 3요인 평균 # psychological_safety: Float, # self_efficacy: Float, # flow_experience: Float # }, # dimension_scores: { # 9요인 평균 # belonging: Float, autonomy: Float, meaning: Float, # recognition: Float, achievement: Float, growth: Float, # vitality: Float, strength_use: Float, positive_affect: Float # }, # group_results: [ # 그룹별 결과 (group이 nil일 때) # { group: WorkspaceGroup, ...위와 동일 구조... } # 3명 이하는 nil # ] # } ``` - 해당 survey의 모든 DiagnosisSession을 조회 - 3명 이하 그룹은 결과를 nil로 반환 (insufficient_responses: true 표시) - overall_score: 각 세션의 overall(10번 문항) 점수 평균 - engagement_types: overall 문항 기준 6,7점=active / 4,5점=passive / 1,2,3점=disengaged 비율 - factor_scores: 3요인별 평균 (EngagementCalculator의 FACTOR_DIMENSION_MAP 참조) - dimension_scores: 9개 dimension별 평균 - 강약점: 가장 높은/낮은 factor, dimension 표시 ### 테스트 - `test/services/engagement_surveys/create_service_test.rb` - 정상 생성, 중복 방지, 필수 필드 누락 - `test/services/engagement_surveys/submit_response_service_test.rb` - 정상 응답 저장, 비활성 설문 거부, user_id 기록, 채점 결과 확인 - `test/services/engagement_surveys/result_aggregator_test.rb` - 전체 집계, 그룹별 집계, 3명 이하 필터링, 몰입 유형 비율, 3요인/9요인 분석 ## 중요 사항 - 모든 rails 명령은 `docker compose exec web` 으로 실행 - TDD: 테스트 먼저 작성 → 구현 - 기존 EngagementCalculator 로직을 최대한 재사용 - Result 패턴 사용 (service-objects 스킬 참조)

D
developer-1
4 months
높음 e36ba067
서브 티켓 [조직문화 PR3] 컨트롤러: 워크스페이스 설문 관리 (CRUD + 결과)

Workspaces::EngagementSurveysController 구현

## 작업: 워크스페이스 설문 관리 컨트롤러 ### 참조 (반드시 읽기) - `.claude/skills/rails-core/SKILL.md` - `.claude/skills/rails-testing/SKILL.md` - 기존 패턴 참고: `app/controllers/workspaces/team_strengths_controller.rb`, `app/controllers/workspaces/settings_controller.rb` - 권한 concern: `app/controllers/concerns/workspace_authorization.rb` - PR2 서비스: `app/services/engagement_surveys/create_service.rb`, `app/services/engagement_surveys/result_aggregator.rb` ### 컨트롤러: `app/controllers/workspaces/engagement_surveys_controller.rb` ```ruby module Workspaces class EngagementSurveysController < ApplicationController include WorkspaceAuthorization # index, show → admin or leader # new, create, toggle_status → admin only end end ``` #### 액션 - **index**: 해당 워크스페이스의 설문 목록 (by_period 스코프) - **new**: 새 설문 생성 폼 (연도/분기 선택) - **create**: EngagementSurveys::CreateService 호출, 성공 시 show로 redirect - **show**: EngagementSurveys::ResultAggregator로 결과 조회, @survey + @result 세팅 - **toggle_status**: draft→active→closed 순서로 상태 전환 #### 권한 매트릭스 | 액션 | member | leader | admin | |------|--------|--------|-------| | index | - | O | O | | new/create | - | - | O | | show | - | O | O | | toggle_status | - | - | O | ### 라우트 추가 (`config/routes.rb`) 기존 workspaces 블록 안에 추가: ```ruby resources :engagement_surveys, only: [:index, :new, :create, :show], controller: "workspaces/engagement_surveys" do member do patch :toggle_status end end ``` ### 테스트: `test/controllers/workspaces/engagement_surveys_controller_test.rb` - index 접근 (leader O, member X, 비로그인 X) - create (admin O, leader X) - show 결과 조회 - toggle_status 상태 전환 - 권한 거부 시 적절한 응답 ### 주의사항 - 뷰 파일은 최소한의 placeholder만 생성 (PR5에서 본격 구현) - 기존 WorkspaceAuthorization 패턴 확인 후 동일하게 적용 - 모든 rails 명령: `docker compose exec web`

D
developer-1
4 months
높음 f76cda8a
서브 티켓 [조직문화 PR4] 공개 응답 플로우: 비인증 설문 응답 컨트롤러

EngagementSurveyResponsesController 공개 응답 플로우 구현

## 작업: 비인증 설문 응답 컨트롤러 + 뷰 ### 참조 (반드시 읽기) - `.claude/skills/rails-core/SKILL.md` - `.claude/skills/rails-testing/SKILL.md` - `.claude/skills/hotwire-patterns/SKILL.md` — Turbo Streams 문항 전환 - `.claude/skills/ui-design/SKILL.md` — 디자인 시스템 - 기존 진단 플로우 참고: `app/controllers/diagnosis_sessions_controller.rb` - 기존 뷰 패턴: `app/views/diagnosis_sessions/engagement_show.html.erb`, `_engagement_question.html.erb`, `_engagement_guide.html.erb` - PR2 서비스: `app/services/engagement_surveys/submit_response_service.rb` - 문항 데이터: `db/seeds/engagement/engagement_questions_b2b.csv` - QR 헬퍼: `app/helpers/qr_code_helper.rb` ### 컨트롤러: `app/controllers/engagement_survey_responses_controller.rb` ```ruby class EngagementSurveyResponsesController < ApplicationController allow_unauthenticated_access layout "focus" # 또는 기존 진단 플로우에서 사용하는 레이아웃 확인 end ``` #### 라우트 (`config/routes.rb`에 추가, workspaces 밖) ```ruby scope "engage/:access_token", controller: "engagement_survey_responses" do get "/", action: :show, as: :engagement_survey_response post "start", action: :start, as: :start_engagement_survey post "submit", action: :submit, as: :submit_engagement_survey get "complete", action: :complete, as: :complete_engagement_survey end ``` #### 액션 - **show** (GET `/engage/:access_token`): - access_token으로 survey 조회, active 상태 확인 - 워크스페이스의 그룹 목록 표시 - 그룹 선택 UI - **start** (POST): - 그룹 선택 확인 - 안내 메시지 표시: - "약 5분이 소요됩니다" - "3명 이하 그룹의 결과는 측정되지 않습니다" - "응답자의 정보는 기록되지 않습니다" - 세션에 그룹 ID 저장 - 문항 페이지로 이동 - **submit** (POST): - 10문항 7점 척도 응답 수집 - EngagementSurveys::SubmitResponseService 호출 - user: Current.user (로그인 시 참고용, nil도 OK) - complete로 redirect - **complete** (GET): - 감사 메시지 + 브라우저 닫기 안내 #### 뷰 파일 (실제 구현) - `app/views/engagement_survey_responses/show.html.erb` — 그룹 선택 - `app/views/engagement_survey_responses/start.html.erb` — 안내 + 문항 시작 - `app/views/engagement_survey_responses/submit.html.erb` — 10문항 7점 척도 (기존 _engagement_question 패턴 참고) - 또는 start 뷰에서 문항까지 한 페이지로 처리 (기존 engagement_show 패턴 확인) - `app/views/engagement_survey_responses/complete.html.erb` — 완료 ### B2B 10문항 데이터 ``` 1. belonging / psychological_safety / "팀에서 소속감을 느꼈다" 2. autonomy / psychological_safety / "업무 방식을 스스로 결정할 수 있었다" 3. meaning / psychological_safety / "내 업무가 의미 있다고 느꼈다" 4. recognition / self_efficacy / "나의 기여를 인정받았다" 5. achievement / self_efficacy / "업무 목표를 달성하고 있다고 느꼈다" 6. growth / self_efficacy / "업무를 통해 성장하고 있다고 느꼈다" 7. vitality / flow_experience / "일할 때 활력이 넘쳤다" 8. strength_use / flow_experience / "업무에서 나의 강점을 발휘했다" 9. positive_affect / flow_experience / "일하면서 긍정적인 감정을 느꼔다" 10. overall / overall / "현재 나의 직장에 만족한다" ``` - 문항은 DB(SurveyQuestion)에서 가져오거나, 하드코딩 (question_type="engagement_b2b") ### 테스트: `test/controllers/engagement_survey_responses_controller_test.rb` - show: access_token으로 접근 가능 - show: 비활성 설문 접근 차단 - start: 그룹 선택 후 안내 표시 - submit: 10문항 응답 제출 성공 - complete: 완료 페이지 렌더링 - 비인증 접근 가능 확인 ### 주의사항 - **파일 충돌 방지**: developer-1이 `workspaces/engagement_surveys_controller.rb`와 라우트를 수정하므로, 이 에이전트는 `engagement_survey_responses_controller.rb`만 생성 - 라우트 추가 시 developer-1과 겹치지 않는 위치에 추가 (workspaces 블록 밖) - 모바일 최적화 (QR 스캔이 주 진입점) - 모든 rails 명령: `docker compose exec web`

D
developer-2
4 months
높음 07fd297c

성능 P0: Admin 사용자 목록 N+1 쿼리 수정

admin/users_controller의 diagnosis_sessions N+1 문제 수정. - includes에 diagnosis_sessions: :diagnosis_type 추가 - 또는 counter_cache 도입 후 모달은 AJAX lazy load로 변경 - diagnosis_sessions 전체 eager load는 메모리 폭발 위험

미배정 4 months
높음 87c8b576

성능 P0: 통계 페이지 GROUP BY 전환

admin/statistics_controller의 calculate_completion_rate_by_type에서 DiagnosisType별 개별 COUNT → 단일 GROUP BY 쿼리로 전환. - DiagnosisSession.group(:diagnosis_type_id).count - .where(status: "completed").group(:diagnosis_type_id).count

미배정 4 months
높음 d0fd66c0

성능 P0: Engagement Survey trend N+1 제거

workspaces/engagement_surveys_controller의 build_trend_data에서 서베이별 ResultAggregator 개별 호출 → diagnosis_sessions eager load + 결과 캐싱

미배정 4 months
보통 087ee199

성능 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]

미배정 4 months
보통 07440567

성능 P1: DiagnosisSession 반복 pluck 캐싱

diagnosis_sessions_controller에서 responses.pluck(:survey_question_id)가 요청당 최대 3~6회 반복 호출됨. - @answered_ids ||= @session.responses.pluck(:survey_question_id).to_set 패턴 적용

미배정 4 months
낮음 d0e9f7dc

성능 P2: Admin 대시보드/통계 캐싱 적용

admin/dashboard 8개 COUNT, admin/statistics 6+ 집계 쿼리에 Rails.cache.fetch 적용 (5분 TTL). - DiagnosisTarget.find_by(slug:) 등 정적 데이터도 캐싱

미배정 4 months
낮음 becfc471

성능 P2: counter_cache 도입 (diagnosis_sessions_count)

users 테이블에 diagnosis_sessions_count 컬럼 추가. - DiagnosisSession에 counter_cache: true 설정 - Admin 사용자 목록에서 .count 쿼리 제거

미배정 4 months

진행 중

0
티켓 없음

리뷰

18
높음 f1692ab1
부모 티켓

[조직문화 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 확인 (없으면 생성) ## 의존성 없음

0/1
팀리드
4 months
높음 783a907f
부모 티켓

[조직문화 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 완료 후

0/1
팀리드
4 months
높음 218dc4da
부모 티켓

[조직문화 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 완료 후

0/1
팀리드
4 months
높음 5bb571c7
부모 티켓

[조직문화 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 완료 후

0/1
팀리드
4 months
높음 2d6027ef

[일괄다운로드 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 생성

팀리드
3 months
높음 e52b4db2

[크레딧통합 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`

팀리드
3 months
높음 6f687bfb

[리드관리 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 강제. 완료 기준: 어드민 사이드바 "리드 관리 > 리드"에서 퍼널별 필터·검색·페이지네이션 동작.

미배정 29 days
높음 e811392f

[일괄다운로드 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 등록

팀리드
3 months
높음 5123232e

[크레딧통합 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`

팀리드
3 months
보통 73725727

[리드관리 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에서 자동 산출된 대상 수·샘플 노출.

미배정 29 days
보통 f96b722b

[일괄다운로드 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 키 추가

팀리드
3 months
높음 4bafdcda

[크레딧통합 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/` 관련 테스트

팀리드
3 months
보통 59abea45

[리드관리 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.

미배정 29 days
높음 479ab05f

[일괄다운로드 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 완료 후 ## 완료 기준 - [ ] 비동기 요청 → 폴링 → 다운로드 플로우 테스트 통과 - [ ] 중복 요청 거부 테스트 통과 - [ ] 인가 테스트 통과

팀리드
3 months
높음 b1e42506

[크레딧통합 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/` 관련 테스트

팀리드
3 months
보통 47c5c109

[리드관리 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 완성.

미배정 29 days
높음 fd35a8f6

[일괄다운로드 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개 언어

팀리드
3 months
보통 de48079b

[크레딧통합 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`

팀리드
3 months

완료 (7일)

0
티켓 없음