본문으로 건너뛰기

약관 및 동의 항목 통합 조회 구현 가이드 (GET /auth/terms-and-consents)

1. 개요

1.1 목적

이 문서는 특정 사용자 플로우(예: 회원가입, 로그인 후 첫 화면 진입)에서 화면에 표시해야 하는 필수 약관 목록선택적 동의 항목 목록 및 현재 상태한 번의 API 호출로 조회하여 제공하는 GET /auth/terms-and-consents 엔드포인트의 구현 가이드라인을 제공합니다.

1.2 역할: 오케스트레이션 (Orchestration)

이 API는 자체적으로 데이터를 소유하거나 핵심 비즈니스 로직을 수행하기보다는, TermsServiceConsentService를 호출하여 각각 약관 정보와 동의 상태 정보를 가져온 뒤, 이를 조합하여 클라이언트(주로 프론트엔드)가 UI를 구성하기 용이한 형태로 가공하여 반환하는 오케스트레이션 역할을 수행합니다.

1.3 관련 문서

TBD

2. 구현 상세

2.1 아키텍처 및 의존성

  • AuthController: API 요청을 받아 처리하고, TermsServiceConsentService를 호출하여 데이터를 조합합니다.
  • TermsService: 활성화된 약관 정보를 조회하는 로직을 제공합니다.
  • ConsentService: 특정 사용자의 현재 동의 상태 또는 기본 동의 항목 정보를 조회하는 로직을 제공합니다.

2.2 Controller 구현 (AuthController 또는 별도 Controller)

// @file: apps/dta-wide-api/src/app/auth/controllers/terms-and-consents.controller.ts
import { Controller, Get, UseGuards, Req, Headers, Inject } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { TermsService, ConsentService } from '@feature/auth-core';
import { TermsAndConsentsDto } from '@shared-contracts/auth/dto';
import { AuthenticatedRequest } from '@shared/interfaces';
import { ApiOperation, ApiResponse, ApiTags, ApiHeader } from '@nestjs/swagger';
import { extractLanguage } from '@shared/utils';

@ApiTags('Auth - Agreements')
@Controller('auth')
export class AuthController {
constructor(@Inject(TermsService) private readonly termsService: TermsService, @Inject(ConsentService) private readonly consentService: ConsentService) {}

@Get('terms-and-consents')
@UseGuards(AuthGuard) // 사용자 인증 필요 시 (또는 앱 토큰 가드)
@ApiOperation({ summary: '필수 약관 및 선택적 동의 항목 통합 조회' })
@ApiHeader({ name: 'Accept-Language', description: '요청 언어 (e.g., de-DE, ko-KR)', required: false })
@ApiResponse({ status: 200, description: '성공', type: TermsAndConsentsDto })
@ApiResponse({ status: 401, description: '인증 실패' })
async getTermsAndConsents(
@Req() request: AuthenticatedRequest, // 인증된 사용자 정보 접근
@Headers('Accept-Language') acceptLanguage: string = 'de-DE' // 기본값 독일어
): Promise<TermsAndConsentsDto> {
// Accept-Language 헤더에서 기본 언어 코드 추출
const language = extractLanguage(acceptLanguage);
const userId = request.user?.id; // 인증된 사용자 ID (없을 수도 있음 - 가입 전)

// 서비스 계층에 위임하여 DTO를 직접 받음
return this.termsService.getTermsAndConsentsDto(language, userId);
}
}

2.3 응답 DTO 정의 (TermsAndConsentsDto)

API 응답 구조를 정의하는 DTO입니다. endpoints.md에 정의된 구조를 따릅니다.

// @file: libs/shared/contracts-auth/dto/src/lib/terms-and-consents.dto.ts
import { ApiProperty } from '@nestjs/swagger';

export class AgreementItemDto {
@ApiProperty({ enum: ['TERMS', 'CONSENT'], description: '항목 유형' })
type: 'TERMS' | 'CONSENT';

@ApiProperty({ description: '항목 고유 식별 키 (TERMS: TermsVersion ID(UUID), CONSENT: Consent Definition Key(string))' })
key: string;

@ApiProperty({ description: '필수 동의 여부' })
required: boolean;

@ApiProperty({ description: '약관 버전 (CONSENT 타입은 null)', nullable: true })
version?: string;

@ApiProperty({ description: '화면에 표시될 현지화된 텍스트' })
text: string;

@ApiProperty({ description: '상세 내용 URL (주로 TERMS 타입)', nullable: true })
detailsUrl?: string;

@ApiProperty({ description: '사용자의 현재 동의 상태 (Terms는 특정 버전에 대한 동의 여부, Consent는 현재 상태)' })
isAgreed: boolean;
}

export class TermsAndConsentsDto {
@ApiProperty({ type: [AgreementItemDto], description: '표시할 약관 및 동의 항목 목록' })
items: AgreementItemDto[];
}

2.4 서비스 계층 구현 (TermsService의 확장)

이 API의 오케스트레이션 로직은 TermsService로 이동하여 컨트롤러를 간소화합니다.

// @file: libs/feature/auth/src/lib/domain/services/terms.service.ts
// 기존 TermsService에 다음 메서드를 추가합니다.

@Injectable()
export class TermsService {
constructor(
private readonly termsRepository: TermsRepository,
private readonly termsVersionRepository: TermsVersionRepository,
private readonly translationRepository: TermsTranslationRepository,
private readonly userAgreementRepository: UserAgreementRepository,
private readonly termsMapper: TermsMapper, // 매퍼 주입
private readonly consentService: ConsentService // 동의 서비스 주입
) {}

// 기존 메서드들...

/**
* 약관 및 동의 항목을 통합 조회하여 DTO로 반환합니다.
* @param language 언어 코드
* @param userId 사용자 ID (없으면 익명 사용자로 간주)
* @returns 약관 및 동의 항목이 통합된 DTO
*/
async getTermsAndConsentsDto(language: string = 'de', userId?: string): Promise<TermsAndConsentsDto> {
// 1. 활성 약관 목록 조회 (DTO 형태)
const activeTermsDto = await this.getActiveTermsDto(language);

// 2. 표시할 동의 항목 목록 및 현재 상태 조회 (DTO 형태)
const displayConsentItemsDto = await this.consentService.getDisplayConsentItemsDto(language, userId);

// 3. 결과 조합 및 DTO 매핑
const responseItems = [];

// 약관 항목 매핑
for (const termsDto of activeTermsDto) {
// 사용자가 이 특정 '버전'에 동의했는지 확인. `userId`가 없으면(가입 전) 무조건 false.
const hasAgreed = userId ? await this.hasUserAgreedToVersion(userId, termsDto.termsVersionId) : false;

responseItems.push({
type: 'TERMS',
key: termsDto.termsVersionId, // 고유 키 (TermsVersion ID - UUID)
isRequired: termsDto.required, // 상위 Terms의 필수 여부
version: termsDto.version, // 버전 번호
text: termsDto.title, // 번역된 제목
detailsUrl: termsDto.contentUrl, // 번역된 상세 내용 URL
isAgreed: hasAgreed, // 가입 시점에는 false, 로그인 후에는 실제 동의 상태
});
}

// 선택적 동의 항목 매핑
for (const consentItemDto of displayConsentItemsDto) {
responseItems.push({
type: 'CONSENT',
key: consentItemDto.key, // 동의 항목 키 (예: 'DATA_ANALYTICS')
isRequired: false, // 선택적 동의는 항상 false
version: null,
text: consentItemDto.text, // 번역된 동의 문구
detailsUrl: consentItemDto.detailsUrl, // 관련 안내 URL
isAgreed: consentItemDto.isAgreed, // 가입 시점에는 false, 로그인 후에는 실제 동의 상태
});
}

return { items: responseItems };
}
}

2.5 동의 서비스의 DTO 변환 메서드

ConsentService에도 DTO 변환 메서드를 추가합니다.

// @file: libs/feature/auth/src/lib/domain/services/consent.service.ts

@Injectable()
export class ConsentService {
constructor(
private readonly consentDefinitionRepository: ConsentDefinitionRepository,
private readonly userConsentRepository: UserConsentRepository,
private readonly consentMapper: ConsentMapper // 매퍼 주입
) {}

// 기존 메서드...

/**
* 화면에 표시할 동의 항목을 DTO 형태로 반환합니다.
*/
async getDisplayConsentItemsDto(language: string = 'de', userId?: string): Promise<DisplayConsentItemDto[]> {
const displayItems = await this.getDisplayConsentItems(language, userId);
return this.consentMapper.toDisplayConsentItemDtoList(displayItems);
}
}

2.6 매퍼 구현

앞서 구현한 TermsMapper와 유사하게 ConsentMapper도 구현합니다.

// @file: libs/feature/auth/src/lib/infrastructure/mappers/consent.mapper.ts
import { Injectable } from '@nestjs/common';
import { DisplayConsentItem } from '../../domain/models/display-consent-item';
import { DisplayConsentItemDto } from '@shared-contracts/auth/dto';

@Injectable()
export class ConsentMapper {
/**
* DisplayConsentItem을 DTO로 변환합니다.
*/
toDisplayConsentItemDto(item: DisplayConsentItem): DisplayConsentItemDto {
return {
key: item.key,
text: item.text,
detailsUrl: item.detailsUrl,
isAgreed: item.isAgreed,
};
}

/**
* DisplayConsentItem 배열을 DTO 배열로 변환합니다.
*/
toDisplayConsentItemDtoList(items: DisplayConsentItem[]): DisplayConsentItemDto[] {
return items.map((item) => this.toDisplayConsentItemDto(item));
}
}

2.7 TermsServiceConsentService 역할

이 API는 TermsServiceConsentService에 의존하여 실제 데이터 조회 및 비즈니스 로직을 위임합니다. 각 서비스는 다음과 같은 역할과 메서드를 제공해야 합니다.

  • TermsService:

    • getActiveTerms(language: string = 'de', type?: TermsType): Promise<TermsVersion[]>: 현재 날짜 기준으로 활성화(status='ACTIVE')된 모든 약관 버전 목록을 반환합니다. 요청된 language에 맞는 번역 정보 (translations) 또는 기본 언어 번역 정보를 포함해야 합니다. 필요시 type으로 필터링할 수 있습니다. TermsVersion 엔티티는 상위 Terms 정보 (terms 관계)를 포함할 수 있습니다.
    • getActiveTermsDto(language: string = 'de', type?: TermsType): Promise<TermsVersionDto[]>: 위와 동일하지만 DTO 형태로 반환합니다.
    • hasUserAgreedToVersion(userId: string, termsVersionId: string): Promise<boolean>: 특정 사용자가 특정 약관 버전 ID에 동의했는지 여부를 확인하여 boolean 값을 반환합니다.
    • getTermsVersionById(versionId: string, language: string = 'de', includeTerms: boolean = true): Promise<TermsVersion>: 특정 ID의 약관 버전을 조회합니다.
    • getTermsVersionByIdDto(versionId: string, language: string = 'de'): Promise<TermsVersionDto>: 위와 동일하지만 DTO 형태로 반환합니다.
    • getTermsAndConsentsDto(language: string = 'de', userId?: string): Promise<TermsAndConsentsDto>: 약관 및 동의 항목을 통합 조회하여 DTO로 반환합니다.
  • ConsentService:

    • getDisplayConsentItems(language: string = 'de', userId?: string): Promise<DisplayConsentItem[]>: 화면에 표시해야 하는 모든 선택적 동의 항목 목록을 반환합니다. 요청된 language에 맞는 번역된 text와 필요한 경우 detailsUrl을 포함해야 합니다. userId가 제공되면, 해당 사용자의 DB에 저장된 현재 동의 상태(isAgreed)를 반영하여 반환합니다. userId가 없거나 해당 사용자의 동의 기록이 없으면 isAgreedfalse로 반환됩니다. DisplayConsentItem 객체에는 key, text, detailsUrl, isAgreed 등의 정보가 포함됩니다.
    • getDisplayConsentItemsDto(language: string = 'de', userId?: string): Promise<DisplayConsentItemDto[]>: 위와 동일하지만 DTO 형태로 반환합니다.
    • getConsentByUserId(userId: string): Promise<UserConsent[]>: 특정 사용자의 모든 동의 기록 배열을 조회합니다. (내부적으로 getDisplayConsentItems에서 사용될 수 있습니다.)

2.8 오류 처리

  • 인증 토큰이 유효하지 않거나 필요한 권한이 없는 경우 AuthGuard 등에서 401 또는 403 오류를 반환합니다.
  • TermsService 또는 ConsentService 내부에서 데이터 조회 실패 등 예외 발생 시 적절한 HTTP 상태 코드(예: 500)와 오류 메시지를 반환하도록 처리합니다.
  • 일관된 오류 형식 사용:
throw new NotFoundException({
code: 1010, // 숫자형 코드
message: 'TERMS_NOT_FOUND', // 영문 대문자 스네이크케이스
detail: '약관을 찾을 수 없습니다.', // 현지화된 메시지
});

자세한 오류 관리 방식은 오류 관리 가이드를 참조하세요.

4. 데이터베이스 초기 데이터 예시 (SQL)

아래는 예시 화면에 표시된 필수 약관(DiGAV)과 선택적 동의 항목(데이터 처리 개선)을 데이터베이스에 추가하기 위한 SQL INSERT 스크립트 예시입니다. 실제 운영 환경에서는 데이터베이스 마이그레이션 도구나 관리 스크립트를 통해 관리하는 것이 좋습니다.

가정:

  • 데이터베이스 스키마는 termconsent를 사용합니다 (Prisma 스키마 기준).
  • 약관 정보는 term.terms, term.terms_versions, term.terms_translations 테이블에 저장됩니다.
  • 동의 항목 정의 및 번역은 consent.consent_definitions, consent.consent_definition_translations 테이블에 저장됩니다.
  • PostgreSQL의 uuid_generate_v4() 함수를 사용합니다. 이 함수는 uuid-ossp 확장 기능이 필요합니다.
    • 만약 uuid-ossp 확장이 활성화되지 않았다면, 데이터베이스 슈퍼유저 권한으로 다음 SQL을 실행해야 합니다: CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
  • 약관 관련 INSERT는 DO 블록을 사용하여 생성된 ID를 전달합니다.
-- NOTE: libs/core/database/prisma/schema.prisma 스키마에 맞춰 업데이트된 SQL
-- NOTE: uuid_generate_v4() 함수 사용 및 DO 블록으로 ID 전달 처리 (uuid-ossp 확장 필요)

-- 1. 약관 삽입 (DiGAV 이용약관) - 단일 트랜잭션으로 처리
DO $$
DECLARE
new_terms_uuid uuid;
new_terms_version_uuid uuid;
BEGIN
--- 1.1. 약관 정의 삽입 (`term.terms`)
INSERT INTO term.terms (id, name, version, status, terms_type, valid_from, valid_until, created_by, created_at, updated_at)
VALUES (
uuid_generate_v4(), -- UUID 자동 생성 (uuid-ossp 확장 함수)
'DiGAV Terms of Service', -- 약관 이름 (관리용)
'1.0', -- 약관 버전
'ACTIVE', -- 약관 상태 (TermsStatus Enum)
'MANDATORY', -- 약관 유형 (TermsType Enum)
NOW(), -- 유효 시작일
'9999-12-31 23:59:59', -- 유효 종료일
'admin_user', -- 생성자
NOW(), -- 생성 시간
NOW() -- 업데이트 시간
) RETURNING id INTO new_terms_uuid; -- 생성된 ID를 변수에 저장

--- 1.2. 약관 버전 삽입 (`term.terms_versions`)
INSERT INTO term.terms_versions (id, terms_id, version_number, status, created_by, created_at, activated_at)
VALUES (
uuid_generate_v4(), -- UUID 자동 생성
new_terms_uuid, -- 위에서 저장된 Terms ID 사용
'1.0', -- 약관 버전 번호
'ACTIVE', -- 상태 (TermsStatus Enum)
'admin_user', -- 생성자
NOW(), -- 생성 시간
NOW() -- 활성화 시간
) RETURNING id INTO new_terms_version_uuid; -- 생성된 ID를 변수에 저장

--- 1.3. 약관 번역 삽입 (`term.terms_translations`)
INSERT INTO term.terms_translations (id, terms_version_id, language, content, created_at, updated_at)
VALUES
-- 독일어 번역
(uuid_generate_v4(), new_terms_version_uuid, 'de', 'Ich stimme den Nutzungsbedingungen gemäß der DiGAV § 4 Abs. 2 zu.', NOW(), NOW()),
-- 한국어 번역
(uuid_generate_v4(), new_terms_version_uuid, 'ko', 'DiGAV § 4 Abs. 2에 따른 이용약관에 동의합니다.', NOW(), NOW()),
-- 영어 번역
(uuid_generate_v4(), new_terms_version_uuid, 'en', 'I agree to the Terms of Use according to DiGAV § 4 Abs. 2.', NOW(), NOW());

END $$;


--- 2. 선택적 동의 항목 정의 및 번역 삽입 (데이터 처리 개선) - 독립적인 트랜잭션

--- 2.1. 동의 항목 정의 삽입 (`consent.consent_definitions`)
INSERT INTO consent.consent_definitions (key, name, default_language, created_at, updated_at)
VALUES
('DATA_ANALYTICS', 'Data Processing Improvement', 'de', NOW(), NOW())
ON CONFLICT (key) DO NOTHING; -- 이미 키가 존재하면 아무 작업 안 함

--- 2.2. 동의 항목 번역 삽입 (`consent.consent_definition_translations`)
INSERT INTO consent.consent_definition_translations (id, consent_definition_key, language, text, details_url, created_at, updated_at)
VALUES
-- 독일어 번역
(uuid_generate_v4(), 'DATA_ANALYTICS', 'de', 'Ich erlaube die Verarbeitung der zu der dauerhaften Gewährleistung der technischen Funktionsfähigkeit, der Nutzerfreundlichkeit und der Weiterentwicklung der digitalen Gesundheitsanwendung.', null, NOW(), NOW()),
-- 한국어 번역
(uuid_generate_v4(), 'DATA_ANALYTICS', 'ko', '기술적 기능성, 사용자 편의성 및 디지털 건강 애플리케이션의 지속적인 개선 및 추가 개발을 위한 데이터 처리를 허용합니다.', null, NOW(), NOW()),
-- 영어 번역
(uuid_generate_v4(), 'DATA_ANALYTICS', 'en', 'I allow the processing of data for the permanent assurance of technical functionality, user-friendliness, and the further development of the digital health application.', null, NOW(), NOW())
ON CONFLICT (consent_definition_key, language) DO UPDATE SET -- 이미 번역이 존재하면 업데이트
text = EXCLUDED.text,
details_url = EXCLUDED.details_url,
updated_at = NOW();

3. 변경 이력

버전날짜작성자변경 내용
0.1.12025-04-30elizabeth@weltcorp.com데이터베이스 초기 데이터 예시 SQL 업데이트 (스키마 반영, uuid-ossp 확장 설명 추가)
0.1.02025-04-22bok@weltcorp.com최초 작성