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 도메인 비즈니스 규칙 문서를 참조하세요.
주요 규칙 카테고리는 다음과 같습니다:
- 설문 관리 규칙: 설문 회차 및 순서, 스킵, 데이터 저장 규칙
- 설문 진행 관리 규칙: 이탈 및 재진입, '나중에 하기' 관리, 진행 상태 관리 규칙
- 설문 응답 관리 규칙: 응답 유형 및 기본값, 유효성 검증 규칙
- 설문 결과 및 피드백 규칙: 점수 계산, 결과 피드백, 이력 관리 규칙
- 최종 보고서 관리 규칙: 보고서 생성, 차트 규칙
- 다국어 지원 규칙: 콘텐츠 제공 규칙
- 설문 스케줄링 규칙: 일정 관리 규칙
- 성능 및 품질 규칙: 성능 요구사항 규칙
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.0 | 2025-05-12 | bok@weltcorp.com | 최초 작성 |
| 0.2.0 | 2025-05-13 | bok@weltcorp.com | 모바일 캐싱을 위한 QuestionnaireBundle 개념 추가 |
| 0.3.0 | 2025-05-13 | bok@weltcorp.com | GDPR 준수 및 데이터 관리 관련 필드, 서비스, 이벤트 추가 |
| 0.3.1 | 2025-05-20 | bok@weltcorp.com | 회차 누적 결과 조회 서비스 및 인터페이스 추가 |