본문으로 건너뛰기

Questionnaire 도메인 모델

데이터 저장 규칙

  • questionnaire 스키마: 설문 정의, 문항, 점수 계산 규칙 등 설문 관련 핵심 정보를 저장합니다.
  • private 스키마: 사용자 응답 데이터 등 개인정보(PII)와 관련된 데이터를 저장합니다. GDPR 등 규정 준수를 위해 접근 제어가 엄격하게 관리되어야 합니다.

1. 엔티티 관계도 (ERD)

2. 엔티티

2.1 Questionnaire

/**
* 설문 정의 엔티티
*/
interface Questionnaire {
id: string;
questionnaireType: QuestionnaireType;
scoreCalculationType: ScoreCalculationType;
maxScore: number;
metadata: QuestionnaireMetadata;
description: string;
version: string; // 설문 버전 정보
isActive: boolean; // 활성화 상태
createdAt: Date;
updatedAt: Date;

// 관계
translations: QuestionnaireTranslation[];
questions: Question[];
scoreLevels: ScoreLevel[];
previousVersions: Questionnaire[]; // 이전 버전 참조
// 부가 정보 표시 규칙
additionalInformationRules?: AdditionalInformationRule[];
}

enum QuestionnaireType {
ISI = 'ISI', // 불면증 심각도 지수
DBAS16 = 'DBAS16', // 수면에 대한 비합리적 신념
PHQ9 = 'PHQ9', // 우울 척도
GAD7 = 'GAD7', // 불안 척도
PSS = 'PSS', // 지각된 스트레스 척도
WIS = 'WIS' // WELT Insomnia Scale
}

enum ScoreCalculationType {
SUM = 'SUM', // 문항 점수 합계
AVERAGE = 'AVERAGE', // 문항 점수 평균
CUSTOM = 'CUSTOM' // 맞춤 계산 로직
}

interface QuestionnaireMetadata {
estimatedTimeMinutes: number; // 예상 소요 시간(분)
purpose: string; // 설문 목적
recommendations: string; // 권장 환경 및 진행 방법
notes: string; // 응답 시 유의사항
versionInfo?: VersionInfo; // 버전 관련 메타데이터
}

interface VersionInfo {
versionNumber: string; // 버전 번호
releaseDate: Date; // 출시 일자
changelog: string; // 변경 사항
}

2.2 QuestionnaireTranslation

/**
* 설문 번역 엔티티
*/
interface QuestionnaireTranslation {
id: string;
questionnaireId: string;
language: string; // 'ko', 'en', 'de'
title: string;
intro: string; // 설문 도입부 번역
description: string; // 공통 질문 설명 번역
postDescription: string; // 설문 완료 후 추가 설명 번역
createdAt: Date;
updatedAt: Date;

// 관계
questionnaire: Questionnaire;
}

2.3 QuestionnaireBundle

/**
* 설문 번들 엔티티 (모바일 캐싱용)
* 활성화된 설문들의 묶음을 저장합니다.
*/
interface QuestionnaireBundle {
id: string; // 번들 ID
generatedAt: Date; // 번들 생성 시간
isActive: boolean; // 활성화 상태
questionnaires: string[]; // 포함된 설문지 ID 목록
createdAt: Date; // 생성 시간
updatedAt: Date; // 수정 시간
}

/**
* 설문 버전 상태 정보
*/
interface QuestionnaireVersionStatus {
questionnaireType: QuestionnaireType;
currentVersion: string; // 현재 활성화된 버전
lastModified: number; // 마지막 수정 시간 (Unix timestamp)
}

2.4 Question

/**
* 설문 문항 엔티티
*/
interface Question {
id: string;
questionnaireId: string;
orderIndex: number; // 표시 순서
questionType: QuestionType;
isRequired: boolean;
metadata: QuestionMetadata;
createdAt: Date;
updatedAt: Date;

// 관계
questionnaire: Questionnaire;
translations: QuestionTranslation[];
options: QuestionOption[];
additionalInformationRules?: AdditionalInformationRule[];
}

enum QuestionType {
LINEAR_SCALE = 'LINEAR_SCALE', // 선형 척도 (0-10)
SINGLE_CHOICE = 'SINGLE_CHOICE', // 범주형 척도
TIME = 'TIME', // 시간 정보
SINGLE_LINE_TEXT = 'SINGLE_LINE_TEXT', // 단일 라인 텍스트
MULTI_LINE_TEXT = 'MULTI_LINE_TEXT' // 다중 라인 텍스트
}

interface QuestionMetadata {
defaultValue?: any; // 기본값
minValue?: number; // 최소값 (숫자형)
maxValue?: number; // 최대값 (숫자형)
maxLength?: number; // 최대 길이 (텍스트형)
step?: number; // 증가/감소 단위 (숫자형)
}

2.5 QuestionTranslation

/**
* 설문 문항 번역 엔티티
*/
interface QuestionTranslation {
id: string;
questionId: string;
language: string; // 'ko', 'en', 'de'
text: string;
description: string;
createdAt: Date;
updatedAt: Date;

// 관계
question: Question;
}

2.6 QuestionOption

/**
* 설문 문항 선택지 엔티티
*/
interface QuestionOption {
id: string;
questionId: string;
value: string; // 저장될 값
orderIndex: number; // 표시 순서
score: number; // 점수 계산용 값
metadata?: any; // 옵션별 추가 UI 정보 (예: 조건부 텍스트 입력)
createdAt: Date;
updatedAt: Date;

// 관계
question: Question;
translations: QuestionOptionTranslation[];
}

2.7 QuestionOptionTranslation

/**
* 설문 문항 선택지 번역 엔티티
*/
interface QuestionOptionTranslation {
id: string;
optionId: string;
language: string; // 'ko', 'en', 'de'
text: string;
createdAt: Date;
updatedAt: Date;

// 관계
option: QuestionOption;
}

2.8 ScoreLevel

/**
* 설문 점수 구간 엔티티
*/
interface ScoreLevel {
id: string;
questionnaireId: string;
minScore: number; // 구간 최소값
maxScore: number; // 구간 최대값
createdAt: Date;
updatedAt: Date;

// 관계
questionnaire: Questionnaire;
translations: ScoreLevelTranslation[];
}

2.9 ScoreLevelTranslation

/**
* 설문 점수 구간 번역 엔티티
*/
interface ScoreLevelTranslation {
id: string;
scoreLevelId: string;
language: string; // 'ko', 'en', 'de'
label: string; // 결과 페이지 라벨
chartLabel: string; // 내 차트 라벨
description: string; // 지표 설명
feedback: string; // 사용자 피드백 메시지
createdAt: Date;
updatedAt: Date;

// 관계
scoreLevel: ScoreLevel;
}

2.10 QuestionnaireRound

/**
* 설문 회차 엔티티
*/
interface QuestionnaireRound {
id: string;
userId: string; // 사용자 ID (User 도메인 참조)
roundNumber: RoundNumber;
status: QuestionnaireRoundStatus;
scheduledAt: Date; // 예약 시점
skippedAt?: Date; // 나중에 하기 선택 시점
validUntil?: Date; // 나중에 하기 시 유효기간
completedAt?: Date; // 완료 시점
createdAt: Date;
updatedAt: Date;

// 관계
responses: QuestionnaireResponse[];
}

enum RoundNumber {
FIRST = 'FIRST', // 1회차 (가입 시)
SECOND = 'SECOND', // 2회차 (5주차)
THIRD = 'THIRD', // 3회차 (9주차)
FOURTH = 'FOURTH' // 4회차 (종료 후)
}

enum QuestionnaireRoundStatus {
PENDING = 'PENDING', // 대기
ACTIVE = 'ACTIVE', // 진행 중
COMPLETED = 'COMPLETED', // 완료
SKIPPED = 'SKIPPED', // 나중에 하기 선택
EXPIRED = 'EXPIRED' // 유효기간 만료
}

2.11 QuestionnaireResponse

/**
* 설문 응답 엔티티
*/
interface QuestionnaireResponse {
id: string;
userId: string; // 사용자 ID (User 도메인 참조)
questionnaireId: string;
questionnaireVersion: string; // 응답한 설문의 버전
roundId: string;
score?: number; // 계산된 점수
scoreLevel?: string; // 점수 구간 라벨
startedAt: Date; // 시작 시점
submittedAt?: Date; // 제출 시점
createdAt: Date;
updatedAt: Date;

// 관계
questionnaire: Questionnaire;
round: QuestionnaireRound;
questionResponses: QuestionResponse[];
archivedAt?: Date; // 데이터 아카이빙 시점
anonymizedAt?: Date; // 데이터 익명화 시점
}

2.12 QuestionResponse

/**
* 문항 응답 엔티티
*/
interface QuestionResponse {
id: string;
responseId: string;
questionId: string;
optionId?: string; // 선택형 질문의 경우
textValue?: string; // 텍스트형 질문의 경우
numericValue?: number; // 숫자형 질문의 경우
timeValue?: string; // 시간형 질문의 경우
createdAt: Date;
updatedAt: Date;

// 관계
response: QuestionnaireResponse;
question: Question;
option?: QuestionOption;
additionalInformationRules?: AdditionalInformationRule[];
}

2.13 AdditionalInformationRule

/**
* 부가 정보 표시 규칙 엔티티
*/
interface AdditionalInformationRule {
id: string;
questionnaireId: string; // 연결된 설문 ID
triggerType: AdditionalInfoTriggerType; // 트리거 유형 (질문 답변 또는 점수 임계값)
questionId?: string; // 질문별 트리거용 (문항 ID)
answerValue?: string; // 질문 답변 트리거용 (값 일치 시)
minScore?: number; // 점수 임계값 트리거용 (이상일 때)
maxScore?: number; // 점수 임계값 트리거용 (이하일 때)
content: string; // 표시될 부가 정보 내용
}

enum AdditionalInfoTriggerType {
AnswerValue = 'AnswerValue', // 특정 답변값일 때
ScoreThreshold = 'ScoreThreshold' // 점수가 일정 임계값 이상일 때
}

3. 값 객체

3.1 QuestionnaireId

type QuestionnaireId = string; // UUID

3.2 QuestionId

type QuestionId = string; // UUID

3.3 ResponseId

type ResponseId = string; // UUID

3.4 Language

type Language = 'ko' | 'en' | 'de' | string; // 지원 언어 코드

3.5 QuestionValue

// 질문 유형별 응답 값 표현
type QuestionValue = string | number | Date | { hours: number; minutes: number } | null;

4. 집계 (Aggregates)

4.1 Questionnaire Aggregate

  • Root: Questionnaire
  • Entities: QuestionnaireTranslation, Question, QuestionTranslation, QuestionOption, QuestionOptionTranslation, ScoreLevel, ScoreLevelTranslation
  • Value Objects: QuestionnaireId, QuestionId, Language
  • 불변식:
    • 모든 설문은 고유한 QuestionnaireId를 가짐.
    • 설문 문항은 최소 1개 이상 존재해야 함.
    • 설문 문항의 순서는 정해져 있음.
    • 설문 유형별 점수 계산 방식이 정의되어 있음.
    • 모든 필수 언어 번역이 제공되어야 함.
    • 점수 구간은 중복되지 않아야 함.

4.2 QuestionnaireRound Aggregate

  • Root: QuestionnaireRound
  • Value Objects: RoundNumber, QuestionnaireRoundStatus
  • 불변식:
    • 모든 설문 회차는 고유한 ID를 가짐.
    • 사용자는 동일한 회차에 중복 참여할 수 없음.
    • 1회차와 4회차는 스킵할 수 없음.
    • 2회차 '나중에 하기'는 42일차까지만 유효.
    • 3회차 '나중에 하기'는 70일차까지만 유효.
    • 스킵한 설문은 ValidUntil 기간 내에만 참여 가능.
    • 상태 전이 규칙 준수: PENDING -> ACTIVE -> (COMPLETED | SKIPPED) -> EXPIRED

4.3 QuestionnaireResponse Aggregate

  • Root: QuestionnaireResponse
  • Entities: QuestionResponse
  • Value Objects: ResponseId, QuestionValue
  • 불변식:
    • 모든 설문 응답은 고유한 ResponseId를 가짐.
    • 제출된 설문 응답은 수정할 수 없음.
    • 모든 필수 문항에 응답해야 제출 가능.
    • 문항 유형별 유효성 검증 규칙 적용.
    • 각 설문 유형별 점수 계산 및 피드백 생성 규칙 적용.
    • 각 응답은 특정 문항과 연결되어야 함.
    • 문항 유형에 맞는 응답 데이터 형식이어야 함.
    • 데이터는 archivedAt 또는 anonymizedAt 필드를 통해 생명주기 상태를 가질 수 있음.

5. 도메인 서비스

5.1 QuestionnaireService

interface QuestionnaireService {
// 설문 관리
createQuestionnaire(data: QuestionnaireCreateData): Promise<Questionnaire>;
updateQuestionnaire(id: string, data: QuestionnaireUpdateData): Promise<Questionnaire>;
deleteQuestionnaire(id: string): Promise<void>;
getQuestionnaireById(id: string): Promise<Questionnaire | null>;
getQuestionnaireByType(type: QuestionnaireType): Promise<Questionnaire | null>;
getAllQuestionnaires(): Promise<Questionnaire[]>;

// 설문 버전 관리
createNewVersion(questionnaireId: string, versionData: VersionData): Promise<Questionnaire>;
activateVersion(questionnaireId: string, version: string): Promise<Questionnaire>;
deactivateVersion(questionnaireId: string, version: string): Promise<Questionnaire>;
getQuestionnaireVersions(questionnaireId: string): Promise<Questionnaire[]>;
getQuestionnaireByVersion(questionnaireId: string, version: string): Promise<Questionnaire | null>;

// 번들 관리 (모바일 캐싱용)
getActiveQuestionnaireBundle(): Promise<QuestionnaireBundle>;
createQuestionnaireBundle(): Promise<QuestionnaireBundle>;
updateQuestionnaireBundle(id: string, data: QuestionnaireBundleUpdateData): Promise<QuestionnaireBundle>;
activateQuestionnaireBundle(id: string): Promise<QuestionnaireBundle>;
deactivateQuestionnaireBundle(id: string): Promise<QuestionnaireBundle>;
getQuestionnaireVersionStatus(types: QuestionnaireType[]): Promise<QuestionnaireVersionStatus[]>;

// 문항 관리
createQuestion(questionnaireId: string, data: QuestionCreateData): Promise<Question>;
updateQuestion(id: string, data: QuestionUpdateData): Promise<Question>;
deleteQuestion(id: string): Promise<void>;
getQuestionById(id: string): Promise<Question | null>;
getQuestionsByQuestionnaireId(questionnaireId: string): Promise<Question[]>;

// 점수 구간 관리
createScoreLevel(questionnaireId: string, data: ScoreLevelCreateData): Promise<ScoreLevel>;
updateScoreLevel(id: string, data: ScoreLevelUpdateData): Promise<ScoreLevel>;
deleteScoreLevel(id: string): Promise<void>;
getScoreLevelsByQuestionnaireId(questionnaireId: string): Promise<ScoreLevel[]>;

// 번역 관리
updateQuestionnaireTranslations(questionnaireId: string, translations: QuestionnaireTranslationData[]): Promise<QuestionnaireTranslation[]>;
updateQuestionTranslations(questionId: string, translations: QuestionTranslationData[]): Promise<QuestionTranslation[]>;
updateScoreLevelTranslations(scoreLevelId: string, translations: ScoreLevelTranslationData[]): Promise<ScoreLevelTranslation[]>;
}

5.2 QuestionnaireRoundService

interface QuestionnaireRoundService {
// 회차 관리
scheduleQuestionnaireRound(userId: string, roundNumber: RoundNumber): Promise<QuestionnaireRound>;
startQuestionnaireRound(roundId: string): Promise<QuestionnaireRound>;
completeQuestionnaireRound(roundId: string): Promise<QuestionnaireRound>;
skipQuestionnaireRound(roundId: string): Promise<QuestionnaireRound>;

// 회차 조회
getQuestionnaireRoundById(id: string): Promise<QuestionnaireRound | null>;
getQuestionnaireRoundsByUserId(userId: string): Promise<QuestionnaireRound[]>;
getActiveQuestionnaireRoundByUserId(userId: string): Promise<QuestionnaireRound | null>;
getPendingQuestionnaireRoundsByUserId(userId: string): Promise<QuestionnaireRound[]>;
getSkippedQuestionnaireRoundsByUserId(userId: string): Promise<QuestionnaireRound[]>;

// 회차 스케줄링 확인
checkUserQuestionnaireDueStatus(userId: string, dayIndex: number): Promise<QuestionnaireScheduleStatus>;
expireSkippedQuestionnaireRounds(): Promise<void>; // 스케줄된 작업
}

interface QuestionnaireScheduleStatus {
isDue: boolean;
dueRoundNumber?: RoundNumber;
hasPendingRounds: boolean;
hasSkippedValidRounds: boolean;
}

5.3 QuestionnaireResponseService

interface QuestionnaireResponseService {
// 응답 관리
submitFullQuestionnaireResponse(userId: string, questionnaireId: string, roundId: string, answers: SubmittedAnswerDto[], feedback?: string, metadata?: any): Promise<QuestionnaireResponse>;

// 응답 조회
getQuestionnaireResponseById(id: string): Promise<QuestionnaireResponse | null>;
getQuestionnaireResponsesByUserId(userId: string): Promise<QuestionnaireResponse[]>;
getQuestionnaireResponsesByRoundId(roundId: string): Promise<QuestionnaireResponse[]>;
getQuestionResponsesByResponseId(responseId: string): Promise<QuestionResponse[]>;

// 응답 유효성 검증
validateQuestionnaireResponse(responseId: string): Promise<ValidationResult>;
}

// SubmittedAnswerDto 정의 시작
/**
* 개별 문항 응답 데이터의 기본 구조입니다.
*/
interface BaseSubmittedAnswerDto {
questionId: string;
/** 문항별 응답 메타데이터 (예: 응답 시간, UI 상호작용 데이터 등) */
metadata?: Record<string, any>;
}

/**
* SINGLE_CHOICE 유형 문항의 응답 DTO입니다.
*/
interface SubmittedSingleChoiceAnswerDto extends BaseSubmittedAnswerDto {
questionType: QuestionType.SINGLE_CHOICE;
/** 선택된 옵션의 값 (QuestionOption.value) */
answer: string;
/** QuestionOption.metadata.requiresTextInput === true 일 때, 사용자가 입력한 추가 텍스트 */
textValue?: string;
}

/**
* LINEAR_SCALE 유형 문항의 응답 DTO입니다.
*/
interface SubmittedLinearScaleAnswerDto extends BaseSubmittedAnswerDto {
questionType: QuestionType.LINEAR_SCALE;
/** 사용자가 선택한 숫자 값 */
answer: number;
}

/**
* TIME 유형 문항의 응답 DTO입니다.
* 시간은 "HH:mm" 형식의 문자열로 전달됩니다.
*/
interface SubmittedTimeAnswerDto extends BaseSubmittedAnswerDto {
questionType: QuestionType.TIME;
/** 사용자가 입력한 시간 ("HH:mm") */
answer: string;
}

/**
* SINGLE_LINE_TEXT 유형 문항의 응답 DTO입니다.
*/
interface SubmittedSingleLineTextAnswerDto extends BaseSubmittedAnswerDto {
questionType: QuestionType.SINGLE_LINE_TEXT;
/** 사용자가 입력한 한 줄 텍스트 */
answer: string;
}

/**
* MULTI_LINE_TEXT 유형 문항의 응답 DTO입니다.
*/
interface SubmittedMultiLineTextAnswerDto extends BaseSubmittedAnswerDto {
questionType: QuestionType.MULTI_LINE_TEXT;
/** 사용자가 입력한 여러 줄 텍스트 */
answer: string;
}

/**
* 모든 개별 문항 응답 DTO의 유니온 타입입니다.
* 클라이언트는 문항의 QuestionType에 맞춰 올바른 DTO를 전송해야 합니다.
*/
type SubmittedAnswerDto =
| SubmittedSingleChoiceAnswerDto
| SubmittedLinearScaleAnswerDto
| SubmittedTimeAnswerDto
| SubmittedSingleLineTextAnswerDto
| SubmittedMultiLineTextAnswerDto;
// SubmittedAnswerDto 정의 끝

interface ValidationResult {
isValid: boolean;
errors?: ValidationError[];
}

interface ValidationError {
questionId: string;
message: string;
}

5.4 ScoreCalculationService

interface ScoreCalculationService {
// 점수 계산
calculateQuestionnaireScore(responseId: string): Promise<number>;
generateQuestionnaireFeedback(responseId: string, language: string): Promise<QuestionnaireFeedback>;

// 보고서 생성
generateQuestionnaireReport(userId: string, roundId: string): Promise<QuestionnaireReport>;
generateFinalReport(userId: string): Promise<FinalReport>;
generateQuestionnaireChart(userId: string, questionnaireType: QuestionnaireType): Promise<QuestionnaireChart>;
exportQuestionnaireReport(userId: string, format: 'PDF'): Promise<string>; // URL 또는 ID 반환

// 누적 결과 조회
getCumulativeQuestionnaireResults(userId: string, questionnaireId: string, roundId: string): Promise<CumulativeQuestionnaireResults>;
}

interface QuestionnaireFeedback {
score: number;
scoreLevel: string;
feedbackMessage: string;
}

interface QuestionnaireReport {
userId: string;
roundId: string;
responses: QuestionnaireResponseSummary[];
charts: QuestionnaireChart[];
timestamp: Date;
}

interface FinalReport {
userId: string;
roundSummaries: RoundSummary[];
charts: QuestionnaireChart[];
trends: TrendAnalysis[];
timestamp: Date;
}

interface QuestionnaireChart {
questionnaireType: QuestionnaireType;
rounds: {
roundNumber: RoundNumber;
score: number;
maxScore: number;
label: string;
}[];
}

interface CumulativeQuestionnaireResults {
userId: string;
questionnaireId: string;
requestedRoundId: string;
roundResults: {
roundId: string;
roundNumber: RoundNumber;
status: QuestionnaireRoundStatus;
responses: QuestionnaireResponseSummary[];
}[];
charts: QuestionnaireChart[];
timestamp: Date;
}

5.5 QuestionnaireDataLifecycleService

/**
* GDPR 및 데이터 생명주기 관리를 위한 서비스
*/
interface QuestionnaireDataLifecycleService {
// 사용자 데이터 아카이빙
archiveUserData(userId: string): Promise<void>;

// 아카이빙된 사용자 데이터 복원
restoreUserData(userId: string): Promise<void>;

// 사용자 데이터 삭제 (탈퇴 또는 요청 시)
deleteUserData(userId: string): Promise<void>;

// 사용자 데이터 익명화
anonymizeUserData(userId: string): Promise<void>;

// 사용자 데이터 내보내기 (데이터 이동성)
exportUserData(userId: string, format: 'JSON' | 'CSV'): Promise<string>; // 파일 경로 또는 내용 반환

// 데이터 보관 기간 만료 처리 (내부 자동 실행)
handleDataRetentionPeriodElapsed(userId: string): Promise<void>;
}

6. 도메인 이벤트

6.1 설문 관리 이벤트

interface QuestionnaireCreatedEvent { questionnaireId: string; questionnaireType: QuestionnaireType; timestamp: Date; }
interface QuestionnaireUpdatedEvent { questionnaireId: string; updatedFields: string[]; timestamp: Date; }
interface QuestionnaireDeletedEvent { questionnaireId: string; timestamp: Date; }
interface QuestionCreatedEvent { questionId: string; questionnaireId: string; timestamp: Date; }
interface QuestionUpdatedEvent { questionId: string; updatedFields: string[]; timestamp: Date; }
interface QuestionDeletedEvent { questionId: string; questionnaireId: string; timestamp: Date; }
interface QuestionnaireRoundScheduledEvent { roundId: string; userId: string; roundNumber: RoundNumber; timestamp: Date; }
interface QuestionnaireRoundStartedEvent { roundId: string; userId: string; timestamp: Date; }
interface QuestionnaireRoundCompletedEvent { roundId: string; userId: string; timestamp: Date; }
interface QuestionnaireRoundSkippedEvent { roundId: string; userId: string; validUntil: Date; timestamp: Date; }
interface QuestionnaireRoundExpiredEvent { roundId: string; userId: string; timestamp: Date; }
interface QuestionnaireVersionCreatedEvent { questionnaireId: string; version: string; timestamp: Date; }
interface QuestionnaireVersionActivatedEvent { questionnaireId: string; version: string; timestamp: Date; }
interface QuestionnaireVersionDeactivatedEvent { questionnaireId: string; version: string; timestamp: Date; }
interface QuestionnaireBundleGeneratedEvent { bundleVersion: number; timestamp: Date; }
interface QuestionnaireVersionStatusChangedEvent { questionnaireType: QuestionnaireType; currentVersion: string; lastModified: number; timestamp: Date; }

6.2 설문 진행 이벤트

interface QuestionnaireResponseStartedEvent { responseId: string; userId: string; questionnaireId: string; roundId: string; timestamp: Date; }
interface QuestionnaireResponseCompletedEvent { responseId: string; userId: string; questionnaireId: string; timestamp: Date; }
interface QuestionnaireResponseValidatedEvent { responseId: string; isValid: boolean; timestamp: Date; }
interface QuestionnaireResponseInvalidEvent { responseId: string; errors: ValidationError[]; timestamp: Date; }

6.3 설문 결과 이벤트

interface QuestionnaireScoreCalculatedEvent { responseId: string; score: number; scoreLevel: string; timestamp: Date; }
interface QuestionnaireFeedbackGeneratedEvent { responseId: string; userId: string; questionnaireId: string; timestamp: Date; }
interface QuestionnaireReportGeneratedEvent { userId: string; roundId: string; timestamp: Date; }
interface FinalReportGeneratedEvent { userId: string; timestamp: Date; }
interface QuestionnaireChartGeneratedEvent { userId: string; questionnaireType: QuestionnaireType; timestamp: Date; }
interface QuestionnaireReportExportedEvent { userId: string; format: string; url: string; timestamp: Date; }

6.4 데이터 관리 이벤트 (GDPR 관련)

interface QuestionnaireUserDataArchivedEvent { userId: string; timestamp: Date; details: { roundIds: string[]; responseIds: string[] } }
interface QuestionnaireUserDataRestoredEvent { userId: string; timestamp: Date; details: { roundIds: string[]; responseIds: string[] } }
interface QuestionnaireUserDataDeletedEvent { userId: string; timestamp: Date; }
interface QuestionnaireUserDataAnonymizedEvent { userId: string; timestamp: Date; details: { roundIds: string[]; responseIds: string[] } }
interface UserDataRetentionPeriodElapsedEvent { userId: string; triggeredAction: 'delete' | 'anonymize'; timestamp: Date; }

7. 도메인 규칙

본 문서에서는 모델 구조에 대한 설명에 중점을 두고 있으며, 상세한 비즈니스 규칙은 Questionnaire 도메인 비즈니스 규칙 문서를 참조하세요.

주요 규칙 카테고리는 다음과 같습니다:

  1. 설문 관리 규칙: 설문 회차 및 순서, 스킵, 데이터 저장 규칙
  2. 설문 진행 관리 규칙: 이탈 및 재진입, '나중에 하기' 관리, 진행 상태 관리 규칙
  3. 설문 응답 관리 규칙: 응답 유형 및 기본값, 유효성 검증 규칙
  4. 설문 결과 및 피드백 규칙: 점수 계산, 결과 피드백, 이력 관리 규칙
  5. 최종 보고서 관리 규칙: 보고서 생성, 차트 규칙
  6. 다국어 지원 규칙: 콘텐츠 제공 규칙
  7. 설문 스케줄링 규칙: 일정 관리 규칙
  8. 성능 및 품질 규칙: 성능 요구사항 규칙

8. 데이터베이스 스키마 (Prisma)

generator client {
provider = "prisma-client-js"
}

datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
schemas = ["questionnaire", "private"]
}

// 설문 유형 열거형
enum QuestionnaireType {
ISI // 불면증 심각도 지수
DBAS16 // 수면에 대한 비합리적 신념
PHQ9 // 우울 척도
GAD7 // 불안 척도
PSS // 지각된 스트레스 척도
WIS // WELT Insomnia Scale
}

// 점수 계산 유형 열거형
enum ScoreCalculationType {
SUM
AVERAGE
CUSTOM
}

// 문항 유형 열거형
enum QuestionType {
LINEAR_SCALE
SINGLE_CHOICE
TIME
SINGLE_LINE_TEXT
MULTI_LINE_TEXT
}

// 회차 번호 열거형
enum RoundNumber {
FIRST
SECOND
THIRD
FOURTH
}

// 회차 상태 열거형
enum QuestionnaireRoundStatus {
PENDING
ACTIVE
COMPLETED
SKIPPED
EXPIRED
}

// 트리거 타입 열거형: 부가 정보 표시 규칙용
enum AdditionalInfoTriggerType {
AnswerValue // 특정 답변값일 때
ScoreThreshold // 점수가 일정 임계값 이상일 때
}

// 설문 모델 (questionnaire 스키마)
model Questionnaire {
id String @id @default(uuid())
questionnaireType QuestionnaireType
scoreCalculationType ScoreCalculationType
maxScore Int
metadata Json?
description String? // 공통 질문 설명
version String @default("1.0.0")
isActive Boolean @default(false)
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")

// 관계
translations QuestionnaireTranslation[]
questions Question[]
scoreLevels ScoreLevel[]
responses QuestionnaireResponse[] @relation("QuestionnaireToResponse")
// 부가 정보 표시 규칙 관계
additionalInformationRules AdditionalInformationRule[]

@@unique([questionnaireType, version])
@@index([questionnaireType, isActive])
@@map("questionnaires")
@@schema("questionnaire")
}

// 설문 번역 모델 (questionnaire 스키마)
model QuestionnaireTranslation {
id String @id @default(uuid())
questionnaireId String @map("questionnaire_id")
language String
title String
intro String // 설문 도입부 번역 추가
description String
postDescription String @map("post_description") // 설문 완료 후 추가 설명
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")

// 관계
questionnaire Questionnaire @relation(fields: [questionnaireId], references: [id], onDelete: Cascade)

@@unique([questionnaireId, language])
@@map("questionnaire_translations")
@@schema("questionnaire")
}

// 설문 문항 모델 (questionnaire 스키마)
model Question {
id String @id @default(uuid())
questionnaireId String @map("questionnaire_id")
orderIndex Int @map("order_index")
questionType QuestionType @map("question_type")
isRequired Boolean @default(true) @map("is_required")
metadata Json?
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")

// 관계
questionnaire Questionnaire @relation(fields: [questionnaireId], references: [id], onDelete: Cascade)
translations QuestionTranslation[]
options QuestionOption[]
responses QuestionResponse[]
// 부가 정보 표시 규칙 관계 (질문 답변 트리거용)
additionalInformationRules AdditionalInformationRule[]

@@index([questionnaireId, orderIndex])
@@map("questions")
@@schema("questionnaire")
}

// 설문 문항 번역 모델 (questionnaire 스키마)
model QuestionTranslation {
id String @id @default(uuid())
questionId String @map("question_id")
language String
text String
description String?
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")

// 관계
question Question @relation(fields: [questionId], references: [id], onDelete: Cascade)

@@unique([questionId, language])
@@map("question_translations")
@@schema("questionnaire")
}

// 설문 문항 선택지 모델 (questionnaire 스키마)
model QuestionOption {
id String @id @default(uuid())
questionId String @map("question_id")
value String
orderIndex Int @map("order_index")
score Int
metadata Json? // 옵션별 추가 UI 정보 (예: 조건부 텍스트 입력)
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")

// 관계
question Question @relation(fields: [questionId], references: [id], onDelete: Cascade)
translations QuestionOptionTranslation[]
responses QuestionResponse[] @relation("QuestionOptionToResponse")

@@index([questionId, orderIndex])
@@map("question_options")
@@schema("questionnaire")
}

// 설문 문항 선택지 번역 모델 (questionnaire 스키마)
model QuestionOptionTranslation {
id String @id @default(uuid())
optionId String @map("option_id")
language String
text String
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")

// 관계
option QuestionOption @relation(fields: [optionId], references: [id], onDelete: Cascade)

@@unique([optionId, language])
@@map("question_option_translations")
@@schema("questionnaire")
}

// 설문 점수 구간 모델 (questionnaire 스키마)
model ScoreLevel {
id String @id @default(uuid())
questionnaireId String @map("questionnaire_id")
minScore Int @map("min_score")
maxScore Int @map("max_score")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")

// 관계
questionnaire Questionnaire @relation(fields: [questionnaireId], references: [id], onDelete: Cascade)
translations ScoreLevelTranslation[]

@@index([questionnaireId, minScore, maxScore])
@@map("score_levels")
@@schema("questionnaire")
}

// 설문 점수 구간 번역 모델 (questionnaire 스키마)
model ScoreLevelTranslation {
id String @id @default(uuid())
scoreLevelId String @map("score_level_id")
language String
label String
chartLabel String @map("chart_label")
description String
feedback String
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")

// 관계
scoreLevel ScoreLevel @relation(fields: [scoreLevelId], references: [id], onDelete: Cascade)

@@unique([scoreLevelId, language])
@@map("score_level_translations")
@@schema("questionnaire")
}

// 설문 회차 모델 (private 스키마)
model QuestionnaireRound {
id String @id @default(uuid())
userId String @map("user_id")
roundNumber RoundNumber @map("round_number")
status QuestionnaireRoundStatus
scheduledAt DateTime @map("scheduled_at")
skippedAt DateTime? @map("skipped_at")
validUntil DateTime? @map("valid_until")
completedAt DateTime? @map("completed_at")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")

// 관계
responses QuestionnaireResponse[]

@@unique([userId, roundNumber])
@@index([userId, status])
@@map("questionnaire_rounds")
@@schema("private")
}

// 설문 응답 모델 (private 스키마)
model QuestionnaireResponse {
id String @id @default(uuid())
userId String @map("user_id")
questionnaireId String @map("questionnaire_id")
questionnaireVersion String @map("questionnaire_version")
roundId String @map("round_id")
score Int?
scoreLevel String? @map("score_level")
startedAt DateTime @map("started_at")
submittedAt DateTime? @map("submitted_at")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")

// 관계
questionnaire Questionnaire @relation("QuestionnaireToResponse", fields: [questionnaireId], references: [id])
round QuestionnaireRound @relation(fields: [roundId], references: [id], onDelete: Cascade)
questionResponses QuestionResponse[]
archivedAt DateTime? @map("archived_at")
anonymizedAt DateTime? @map("anonymized_at")

@@index([userId, roundId])
@@index([questionnaireId, roundId])
@@map("questionnaire_responses")
@@schema("private")
}

// 문항 응답 모델 (private 스키마)
model QuestionResponse {
id String @id @default(uuid())
responseId String @map("response_id")
questionId String @map("question_id")
optionId String? @map("option_id")
textValue String? @map("text_value")
numericValue Float? @map("numeric_value")
timeValue String? @map("time_value")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")

// 관계
response QuestionnaireResponse @relation(fields: [responseId], references: [id], onDelete: Cascade)
question Question @relation(fields: [questionId], references: [id])
option QuestionOption? @relation("QuestionOptionToResponse", fields: [optionId], references: [id])
// 부가 정보 표시 규칙 관계 (질문 답변 트리거용)
additionalInformationRules AdditionalInformationRule[]

@@unique([responseId, questionId])
@@map("question_responses")
@@schema("private")
}

// questionnaire 스키마에 QuestionnaireBundle 모델 추가
model QuestionnaireBundle {
id String @id @default(uuid())
generatedAt DateTime @default(now()) @map("generated_at")
isActive Boolean @default(false) @map("is_active")
questionnaires Json // 포함된 설문지 ID 및 버전 정보
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")

@@index([isActive])
@@map("questionnaire_bundles")
@@schema("questionnaire")
}

// 부가 정보 표시 규칙 모델
model AdditionalInformationRule {
id String @id @default(uuid())
questionnaireId String @map("questionnaire_id")
triggerType AdditionalInfoTriggerType
questionId String? @map("question_id")
answerValue String? @map("answer_value")
minScore Int? @map("min_score")
maxScore Int? @map("max_score")
content String
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")

// 관계
questionnaire Questionnaire @relation(fields: [questionnaireId], references: [id], onDelete: Cascade)
question Question? @relation(fields: [questionId], references: [id], onDelete: Cascade)

@@map("additional_information_rules")
@@schema("questionnaire")
}

9. 변경 이력

버전날짜작성자변경 내용
0.1.02025-05-12bok@weltcorp.com최초 작성
0.2.02025-05-13bok@weltcorp.com모바일 캐싱을 위한 QuestionnaireBundle 개념 추가
0.3.02025-05-13bok@weltcorp.comGDPR 준수 및 데이터 관리 관련 필드, 서비스, 이벤트 추가
0.3.12025-05-20bok@weltcorp.com회차 누적 결과 조회 서비스 및 인터페이스 추가