From b106e40631ddf7938e12de375fd132e9139318a2 Mon Sep 17 00:00:00 2001 From: devmrko Date: Thu, 23 Jul 2026 10:29:22 +0900 Subject: [PATCH] refs #703: route schema metadata through MyBatis --- .../README.md | 6 + .../SchemaMetadataAnnotationRow.java | 11 ++ .../SchemaMetadataColumnRow.java | 12 ++ .../mapper/SchemaMetadataMapper.java | 87 +++++++++ .../service/SchemaMetadataService.java | 178 +++++++----------- .../resources/mapper/SchemaMetadataMapper.xml | 103 ++++++++++ .../service/SchemaMetadataServiceTest.java | 101 ++++++++++ 7 files changed, 384 insertions(+), 114 deletions(-) create mode 100644 src/main/java/com/cloudhandson/vpdbackoffice/domain/schemametadata/SchemaMetadataAnnotationRow.java create mode 100644 src/main/java/com/cloudhandson/vpdbackoffice/domain/schemametadata/SchemaMetadataColumnRow.java create mode 100644 src/main/java/com/cloudhandson/vpdbackoffice/mapper/SchemaMetadataMapper.java create mode 100644 src/main/resources/mapper/SchemaMetadataMapper.xml create mode 100644 src/test/java/com/cloudhandson/vpdbackoffice/service/SchemaMetadataServiceTest.java diff --git a/docs/design/703-smilegate-backoffice-finalization/README.md b/docs/design/703-smilegate-backoffice-finalization/README.md index d0f5be2..80c00ba 100644 --- a/docs/design/703-smilegate-backoffice-finalization/README.md +++ b/docs/design/703-smilegate-backoffice-finalization/README.md @@ -24,6 +24,7 @@ 4. Data Redaction 동기화는 `SGMP_POC`의 실제 게임 사용자·판매 데이터 컬럼만 관리 대상으로 삼는다. 5. 내부 호환용 `CB_*` 뷰와 과거 SQL 이력은 실행 경로에서 제외한다. Smilegate 공개 화면·MCP 설정은 이력의 고객 데이터나 endpoint를 참조하지 않는다. 6. Streamlit 외피는 Smilegate 프로필·게임 데이터 시나리오·`oracle.select_ai.smilegate_game_text2sql` MCP 하나만 노출한다. 이전 고객용 토큰 프리셋 및 감사·보안관리 탭은 기본 실행 경로에서 제외한다. +7. `/schema-metadata`의 테이블 comment·컬럼 comment·annotation 조회와 저장 DDL은 모두 `SchemaMetadataMapper`로 수행한다. 메타데이터 조회는 `SGMP_POC` owner와 허용된 테이블 목록으로 한정한다. ## 설계 결정 @@ -51,6 +52,10 @@ MCP tool은 `SGMP_POC_HAIKU45` 프로파일을 기준으로 게임 데이터의 마스킹 동기화 대상 owner는 `SGMP_POC`다. 관리 정책은 실제 컬럼 존재 여부를 검증한 뒤 사용자 식별자와 거래 사용자 식별자에만 적용한다. 대상에 없는 규칙은 DBMS_REDACT 호출 전에 화면 설정 오류로 처리한다. +### 5. 스키마 메타데이터 접근은 MyBatis로 통일한다 + +`/schema-metadata`는 화면 카드 목록을 정적 허용 목록에서 만들고, 선택된 테이블의 comment·컬럼·annotation만 조회한다. 서비스 계층에는 JDBC 직접 실행을 두지 않는다. MyBatis mapper의 모든 사전 조회는 `owner = 'SGMP_POC'` 조건을 갖고, DDL에 쓰이는 테이블·컬럼·annotation 이름은 호출 전에 대문자 식별자 규칙과 허용 테이블 목록으로 검증한다. + ## 변경 파일과 책임 | 영역 | 파일 | 변경 | @@ -61,6 +66,7 @@ MCP tool은 `SGMP_POC_HAIKU45` 프로파일을 기준으로 게임 데이터의 | MCP 화면 | `templates/mcp-sse.html`, `McpSseService.java`, `SmilegateSelectAiService.java` | 게임 데이터 Select AI 도구, 토큰 검증 및 SHOWSQL 생성 | | 보안 스크립트 화면 | `SecuritySqlScriptService.java` | UI에 노출되는 KB 설명을 게임 데이터 설명으로 교체 | | Streamlit 외피 | `poc4_active_source_20260714/config/`, `apps/poc4/mcp_discovery_ui.py` | Smilegate 로그인/헤더/시나리오와 단일 게임 Text2SQL MCP 계약 적용 | +| 스키마 메타데이터 | `SchemaMetadataService.java`, `SchemaMetadataMapper.java`, `SchemaMetadataMapper.xml` | 직접 JDBC 제거, MyBatis 조회·DDL 통일, `SGMP_POC` owner 조건 강제 | ## 완료 기준 diff --git a/src/main/java/com/cloudhandson/vpdbackoffice/domain/schemametadata/SchemaMetadataAnnotationRow.java b/src/main/java/com/cloudhandson/vpdbackoffice/domain/schemametadata/SchemaMetadataAnnotationRow.java new file mode 100644 index 0000000..37b85ee --- /dev/null +++ b/src/main/java/com/cloudhandson/vpdbackoffice/domain/schemametadata/SchemaMetadataAnnotationRow.java @@ -0,0 +1,11 @@ +package com.cloudhandson.vpdbackoffice.domain.schemametadata; + +/** + * Database row from ALL_ANNOTATIONS_USAGE for one table or column annotation. + */ +public record SchemaMetadataAnnotationRow( + String columnName, + String annotationName, + String annotationValue +) { +} diff --git a/src/main/java/com/cloudhandson/vpdbackoffice/domain/schemametadata/SchemaMetadataColumnRow.java b/src/main/java/com/cloudhandson/vpdbackoffice/domain/schemametadata/SchemaMetadataColumnRow.java new file mode 100644 index 0000000..c767b51 --- /dev/null +++ b/src/main/java/com/cloudhandson/vpdbackoffice/domain/schemametadata/SchemaMetadataColumnRow.java @@ -0,0 +1,12 @@ +package com.cloudhandson.vpdbackoffice.domain.schemametadata; + +/** + * Database row used only while assembling the schema metadata screen. + */ +public record SchemaMetadataColumnRow( + String columnName, + String dataType, + String nullable, + String comment +) { +} diff --git a/src/main/java/com/cloudhandson/vpdbackoffice/mapper/SchemaMetadataMapper.java b/src/main/java/com/cloudhandson/vpdbackoffice/mapper/SchemaMetadataMapper.java new file mode 100644 index 0000000..ffaac61 --- /dev/null +++ b/src/main/java/com/cloudhandson/vpdbackoffice/mapper/SchemaMetadataMapper.java @@ -0,0 +1,87 @@ +package com.cloudhandson.vpdbackoffice.mapper; + +import com.cloudhandson.vpdbackoffice.domain.schemametadata.SchemaMetadataAnnotationRow; +import com.cloudhandson.vpdbackoffice.domain.schemametadata.SchemaMetadataColumnRow; +import java.util.List; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +/** + * All database access for the schema metadata screen. Identifier parameters + * are validated against the screen's closed table list before mapper calls. + */ +@Mapper +public interface SchemaMetadataMapper { + + String findTableComment(@Param("owner") String owner, @Param("tableName") String tableName); + + List findColumns( + @Param("owner") String owner, + @Param("tableName") String tableName + ); + + List findAnnotations( + @Param("owner") String owner, + @Param("tableName") String tableName + ); + + int countColumn( + @Param("owner") String owner, + @Param("tableName") String tableName, + @Param("columnName") String columnName + ); + + int countTableAnnotation( + @Param("owner") String owner, + @Param("tableName") String tableName, + @Param("annotationName") String annotationName + ); + + int countColumnAnnotation( + @Param("owner") String owner, + @Param("tableName") String tableName, + @Param("columnName") String columnName, + @Param("annotationName") String annotationName + ); + + void updateTableComment( + @Param("owner") String owner, + @Param("tableName") String tableName, + @Param("commentLiteral") String commentLiteral + ); + + void updateColumnComment( + @Param("owner") String owner, + @Param("tableName") String tableName, + @Param("columnName") String columnName, + @Param("commentLiteral") String commentLiteral + ); + + void dropTableAnnotation( + @Param("owner") String owner, + @Param("tableName") String tableName, + @Param("annotationName") String annotationName + ); + + void addTableAnnotation( + @Param("owner") String owner, + @Param("tableName") String tableName, + @Param("annotationName") String annotationName, + @Param("annotationValueLiteral") String annotationValueLiteral + ); + + void dropColumnAnnotation( + @Param("owner") String owner, + @Param("tableName") String tableName, + @Param("columnName") String columnName, + @Param("annotationName") String annotationName + ); + + void addColumnAnnotation( + @Param("owner") String owner, + @Param("tableName") String tableName, + @Param("columnName") String columnName, + @Param("annotationName") String annotationName, + @Param("annotationValueLiteral") String annotationValueLiteral + ); +} diff --git a/src/main/java/com/cloudhandson/vpdbackoffice/service/SchemaMetadataService.java b/src/main/java/com/cloudhandson/vpdbackoffice/service/SchemaMetadataService.java index e818362..5e7641f 100644 --- a/src/main/java/com/cloudhandson/vpdbackoffice/service/SchemaMetadataService.java +++ b/src/main/java/com/cloudhandson/vpdbackoffice/service/SchemaMetadataService.java @@ -1,20 +1,25 @@ package com.cloudhandson.vpdbackoffice.service; import com.cloudhandson.vpdbackoffice.domain.schemametadata.SchemaAnnotation; +import com.cloudhandson.vpdbackoffice.domain.schemametadata.SchemaMetadataAnnotationRow; import com.cloudhandson.vpdbackoffice.domain.schemametadata.SchemaMetadataColumn; +import com.cloudhandson.vpdbackoffice.domain.schemametadata.SchemaMetadataColumnRow; import com.cloudhandson.vpdbackoffice.domain.schemametadata.SchemaMetadataView; import com.cloudhandson.vpdbackoffice.domain.structured.StructuredDataTable; +import com.cloudhandson.vpdbackoffice.mapper.SchemaMetadataMapper; import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.regex.Pattern; -import org.springframework.dao.DataAccessException; -import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +/** + * Screen-level orchestration for schema metadata. Database reads and DDL are + * intentionally delegated only to {@link SchemaMetadataMapper}. + */ @Service public class SchemaMetadataService { @@ -23,11 +28,14 @@ public class SchemaMetadataService { private static final int MAX_ANNOTATION_VALUE_LENGTH = 4000; private static final Pattern ORACLE_SIMPLE_NAME = Pattern.compile("[A-Z][A-Z0-9_$#]{0,127}"); - private final JdbcTemplate jdbcTemplate; + private final SchemaMetadataMapper mapper; private final StructuredDataService structuredDataService; - public SchemaMetadataService(JdbcTemplate jdbcTemplate, StructuredDataService structuredDataService) { - this.jdbcTemplate = jdbcTemplate; + public SchemaMetadataService( + SchemaMetadataMapper mapper, + StructuredDataService structuredDataService + ) { + this.mapper = mapper; this.structuredDataService = structuredDataService; } @@ -41,13 +49,12 @@ public class SchemaMetadataService { public SchemaMetadataView find(String tableKey) { StructuredDataTable table = structuredDataService.requireTable(tableKey); - String tableName = table.tableName(); - String tableComment = tableComment(tableName); + String tableName = requireSimpleName(table.tableName(), "table name"); Map> annotations = annotationsByTarget(tableName); List columns = columns(tableName, annotations); return new SchemaMetadataView( table, - nullToEmpty(tableComment), + nullToEmpty(mapper.findTableComment(OWNER, tableName)), annotations.getOrDefault(tableTargetKey(), List.of()), columns ); @@ -56,24 +63,24 @@ public class SchemaMetadataService { @Transactional public void updateTableComment(String tableKey, String comment) { StructuredDataTable table = structuredDataService.requireTable(tableKey); + String tableName = requireSimpleName(table.tableName(), "table name"); String normalizedComment = normalizeText(comment, MAX_COMMENT_LENGTH, "테이블 comment"); - jdbcTemplate.execute("COMMENT ON TABLE " + qualifiedTable(table.tableName()) - + " IS " + quoteLiteral(normalizedComment)); + mapper.updateTableComment(OWNER, tableName, quoteLiteral(normalizedComment)); } @Transactional public void updateColumnComment(String tableKey, String columnName, String comment) { StructuredDataTable table = structuredDataService.requireTable(tableKey); - String column = requireColumn(table.tableName(), columnName); + String tableName = requireSimpleName(table.tableName(), "table name"); + String column = requireColumn(tableName, columnName); String normalizedComment = normalizeText(comment, MAX_COMMENT_LENGTH, "컬럼 comment"); - jdbcTemplate.execute("COMMENT ON COLUMN " + qualifiedTable(table.tableName()) + "." - + quoteName(column) + " IS " + quoteLiteral(normalizedComment)); + mapper.updateColumnComment(OWNER, tableName, column, quoteLiteral(normalizedComment)); } @Transactional public void updateTableAnnotation(String tableKey, String annotationName, String annotationValue) { StructuredDataTable table = structuredDataService.requireTable(tableKey); - updateAnnotation(table.tableName(), null, annotationName, annotationValue); + updateAnnotation(requireSimpleName(table.tableName(), "table name"), null, annotationName, annotationValue); } @Transactional @@ -84,8 +91,9 @@ public class SchemaMetadataService { String annotationValue ) { StructuredDataTable table = structuredDataService.requireTable(tableKey); - String column = requireColumn(table.tableName(), columnName); - updateAnnotation(table.tableName(), column, annotationName, annotationValue); + String tableName = requireSimpleName(table.tableName(), "table name"); + String column = requireColumn(tableName, columnName); + updateAnnotation(tableName, column, annotationName, annotationValue); } private void updateAnnotation( @@ -97,75 +105,55 @@ public class SchemaMetadataService { String key = requireSimpleName(annotationName, "annotation name"); String value = normalizeText(annotationValue, MAX_ANNOTATION_VALUE_LENGTH, "annotation value"); if (annotationExists(tableName, columnName, key)) { - jdbcTemplate.execute(annotationSql(tableName, columnName, "DROP " + quoteName(key))); + if (columnName == null) { + mapper.dropTableAnnotation(OWNER, tableName, key); + } else { + mapper.dropColumnAnnotation(OWNER, tableName, columnName, key); + } } if (!value.isBlank()) { - jdbcTemplate.execute(annotationSql(tableName, columnName, - "ADD " + quoteName(key) + " " + quoteLiteral(value))); + if (columnName == null) { + mapper.addTableAnnotation(OWNER, tableName, key, quoteLiteral(value)); + } else { + mapper.addColumnAnnotation(OWNER, tableName, columnName, key, quoteLiteral(value)); + } } } - private String tableComment(String tableName) { - List values = jdbcTemplate.query(""" - SELECT comments - FROM all_tab_comments - WHERE owner = ? - AND table_name = ? - """, (rs, rowNum) -> rs.getString(1), OWNER, tableName); - return values.isEmpty() ? "" : values.getFirst(); - } - private List columns( String tableName, Map> annotations ) { - return jdbcTemplate.query(""" - SELECT c.column_name, - CASE - WHEN c.data_type IN ('VARCHAR2', 'CHAR', 'NVARCHAR2', 'NCHAR') - THEN c.data_type || '(' || c.char_length || ')' - WHEN c.data_type = 'NUMBER' AND c.data_precision IS NOT NULL AND c.data_scale IS NOT NULL - THEN c.data_type || '(' || c.data_precision || ',' || c.data_scale || ')' - WHEN c.data_type = 'NUMBER' AND c.data_precision IS NOT NULL - THEN c.data_type || '(' || c.data_precision || ')' - ELSE c.data_type - END AS display_type, - c.nullable, - cc.comments - FROM all_tab_columns c - LEFT JOIN all_col_comments cc - ON cc.owner = c.owner - AND cc.table_name = c.table_name - AND cc.column_name = c.column_name - WHERE c.owner = ? - AND c.table_name = ? - ORDER BY c.column_id - """, (rs, rowNum) -> new SchemaMetadataColumn( - rs.getString("column_name"), - rs.getString("display_type"), - "Y".equalsIgnoreCase(rs.getString("nullable")), - nullToEmpty(rs.getString("comments")), - annotations.getOrDefault(columnTargetKey(rs.getString("column_name")), List.of()) - ), OWNER, tableName); + return mapper.findColumns(OWNER, tableName).stream() + .map(row -> toColumn(row, annotations)) + .toList(); + } + + private SchemaMetadataColumn toColumn( + SchemaMetadataColumnRow row, + Map> annotations + ) { + String columnName = requireSimpleName(row.columnName(), "column name"); + return new SchemaMetadataColumn( + columnName, + row.dataType(), + "Y".equalsIgnoreCase(row.nullable()), + nullToEmpty(row.comment()), + annotations.getOrDefault(columnTargetKey(columnName), List.of()) + ); } private Map> annotationsByTarget(String tableName) { Map>> grouped = new LinkedHashMap<>(); - jdbcTemplate.query(""" - SELECT column_name, annotation_name, annotation_value - FROM all_annotations_usage - WHERE object_name = ? - AND object_type = 'TABLE' - ORDER BY column_name NULLS FIRST, annotation_name, annotation_value - """, rs -> { - String target = rs.getString("column_name") == null - ? tableTargetKey() - : columnTargetKey(rs.getString("column_name")); - grouped - .computeIfAbsent(target, ignored -> new LinkedHashMap<>()) - .computeIfAbsent(rs.getString("annotation_name"), ignored -> new ArrayList<>()) - .add(nullToEmpty(rs.getString("annotation_value"))); - }, tableName); + for (SchemaMetadataAnnotationRow row : mapper.findAnnotations(OWNER, tableName)) { + String target = row.columnName() == null + ? tableTargetKey() + : columnTargetKey(row.columnName()); + grouped + .computeIfAbsent(target, ignored -> new LinkedHashMap<>()) + .computeIfAbsent(row.annotationName(), ignored -> new ArrayList<>()) + .add(nullToEmpty(row.annotationValue())); + } Map> result = new LinkedHashMap<>(); grouped.forEach((target, valuesByName) -> { @@ -180,44 +168,14 @@ public class SchemaMetadataService { } private boolean annotationExists(String tableName, String columnName, String annotationName) { - Integer count = columnName == null - ? jdbcTemplate.queryForObject(""" - SELECT COUNT(*) - FROM all_annotations_usage - WHERE object_name = ? - AND object_type = 'TABLE' - AND annotation_name = ? - AND column_name IS NULL - """, Integer.class, tableName, annotationName) - : jdbcTemplate.queryForObject(""" - SELECT COUNT(*) - FROM all_annotations_usage - WHERE object_name = ? - AND object_type = 'TABLE' - AND annotation_name = ? - AND column_name = ? - """, Integer.class, tableName, annotationName, columnName); - return count != null && count > 0; - } - - private String annotationSql(String tableName, String columnName, String operation) { - if (columnName == null) { - return "ALTER TABLE " + qualifiedTable(tableName) + " ANNOTATIONS (" + operation + ")"; - } - return "ALTER TABLE " + qualifiedTable(tableName) + " MODIFY " + quoteName(columnName) - + " ANNOTATIONS (" + operation + ")"; + return columnName == null + ? mapper.countTableAnnotation(OWNER, tableName, annotationName) > 0 + : mapper.countColumnAnnotation(OWNER, tableName, columnName, annotationName) > 0; } private String requireColumn(String tableName, String columnName) { String column = requireSimpleName(columnName, "column name"); - Integer count = jdbcTemplate.queryForObject(""" - SELECT COUNT(*) - FROM all_tab_columns - WHERE owner = ? - AND table_name = ? - AND column_name = ? - """, Integer.class, OWNER, tableName, column); - if (count == null || count == 0) { + if (mapper.countColumn(OWNER, tableName, column) == 0) { throw new AppException("선택한 테이블에 존재하지 않는 컬럼입니다."); } return column; @@ -242,14 +200,6 @@ public class SchemaMetadataService { return normalized; } - private String qualifiedTable(String tableName) { - return quoteName(OWNER) + "." + quoteName(requireSimpleName(tableName, "table name")); - } - - private String quoteName(String value) { - return "\"" + value.replace("\"", "\"\"") + "\""; - } - private String quoteLiteral(String value) { return "'" + value.replace("'", "''") + "'"; } diff --git a/src/main/resources/mapper/SchemaMetadataMapper.xml b/src/main/resources/mapper/SchemaMetadataMapper.xml new file mode 100644 index 0000000..1050d38 --- /dev/null +++ b/src/main/resources/mapper/SchemaMetadataMapper.xml @@ -0,0 +1,103 @@ + + + + + + + + + + + + + + + + + + COMMENT ON TABLE "${owner}"."${tableName}" IS ${commentLiteral} + + + + COMMENT ON COLUMN "${owner}"."${tableName}"."${columnName}" IS ${commentLiteral} + + + + ALTER TABLE "${owner}"."${tableName}" ANNOTATIONS (DROP "${annotationName}") + + + + ALTER TABLE "${owner}"."${tableName}" + ANNOTATIONS (ADD "${annotationName}" ${annotationValueLiteral}) + + + + ALTER TABLE "${owner}"."${tableName}" MODIFY "${columnName}" + ANNOTATIONS (DROP "${annotationName}") + + + + ALTER TABLE "${owner}"."${tableName}" MODIFY "${columnName}" + ANNOTATIONS (ADD "${annotationName}" ${annotationValueLiteral}) + + diff --git a/src/test/java/com/cloudhandson/vpdbackoffice/service/SchemaMetadataServiceTest.java b/src/test/java/com/cloudhandson/vpdbackoffice/service/SchemaMetadataServiceTest.java new file mode 100644 index 0000000..0408dd5 --- /dev/null +++ b/src/test/java/com/cloudhandson/vpdbackoffice/service/SchemaMetadataServiceTest.java @@ -0,0 +1,101 @@ +package com.cloudhandson.vpdbackoffice.service; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.cloudhandson.vpdbackoffice.domain.schemametadata.SchemaMetadataAnnotationRow; +import com.cloudhandson.vpdbackoffice.domain.schemametadata.SchemaMetadataColumnRow; +import com.cloudhandson.vpdbackoffice.domain.schemametadata.SchemaMetadataView; +import com.cloudhandson.vpdbackoffice.domain.structured.StructuredDataTable; +import com.cloudhandson.vpdbackoffice.mapper.SchemaMetadataMapper; +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class SchemaMetadataServiceTest { + + private static final StructuredDataTable GAME_USERS = new StructuredDataTable( + "game-users", "CZN_COMN_USER_MST", "게임 사용자", "카제나 게임 사용자 마스터"); + + private SchemaMetadataMapper mapper; + private SchemaMetadataService service; + + @BeforeEach + void setUp() { + mapper = mock(SchemaMetadataMapper.class); + StructuredDataService structuredDataService = mock(StructuredDataService.class); + when(structuredDataService.requireTable("game-users")).thenReturn(GAME_USERS); + when(structuredDataService.tables()).thenReturn(List.of(GAME_USERS)); + when(structuredDataService.defaultKey()).thenReturn("game-users"); + service = new SchemaMetadataService(mapper, structuredDataService); + } + + @Test + void readsOnlyTheSelectedSmilegateTableThroughMapper() { + when(mapper.findTableComment("SGMP_POC", "CZN_COMN_USER_MST")) + .thenReturn("게임별 사용자 기준 정보"); + when(mapper.findAnnotations("SGMP_POC", "CZN_COMN_USER_MST")).thenReturn(List.of( + new SchemaMetadataAnnotationRow(null, "BUSINESS_TERM", "게임 사용자"), + new SchemaMetadataAnnotationRow("GUID", "BUSINESS_TERM", "사용자 고유 식별자"), + new SchemaMetadataAnnotationRow("GUID", "BUSINESS_TERM", "고객 계정 식별자") + )); + when(mapper.findColumns("SGMP_POC", "CZN_COMN_USER_MST")).thenReturn(List.of( + new SchemaMetadataColumnRow("GUID", "VARCHAR2(64)", "N", "사용자 GUID") + )); + + SchemaMetadataView view = service.find("game-users"); + + assertThat(view.tableComment()).isEqualTo("게임별 사용자 기준 정보"); + assertThat(view.tableAnnotations()).singleElement() + .extracting(annotation -> annotation.name(), annotation -> annotation.value()) + .containsExactly("BUSINESS_TERM", "게임 사용자"); + assertThat(view.columns()).singleElement() + .satisfies(column -> { + assertThat(column.columnName()).isEqualTo("GUID"); + assertThat(column.nullable()).isFalse(); + assertThat(column.annotations()).singleElement() + .extracting(annotation -> annotation.value()) + .isEqualTo("사용자 고유 식별자\n--- duplicate annotation value ---\n고객 계정 식별자"); + }); + verify(mapper).findAnnotations("SGMP_POC", "CZN_COMN_USER_MST"); + verify(mapper).findColumns("SGMP_POC", "CZN_COMN_USER_MST"); + } + + @Test + void replacesAnExistingColumnAnnotationUsingMapperDdl() { + when(mapper.countColumn("SGMP_POC", "CZN_COMN_USER_MST", "GUID")).thenReturn(1); + when(mapper.countColumnAnnotation("SGMP_POC", "CZN_COMN_USER_MST", "GUID", "BUSINESS_TERM")) + .thenReturn(1); + + service.updateColumnAnnotation("game-users", "guid", "business_term", "사용자 고유 식별자"); + + verify(mapper).dropColumnAnnotation("SGMP_POC", "CZN_COMN_USER_MST", "GUID", "BUSINESS_TERM"); + verify(mapper).addColumnAnnotation( + "SGMP_POC", "CZN_COMN_USER_MST", "GUID", "BUSINESS_TERM", "'사용자 고유 식별자'"); + } + + @Test + void clearsAnExistingTableAnnotationWithoutAddingAnEmptyValue() { + when(mapper.countTableAnnotation("SGMP_POC", "CZN_COMN_USER_MST", "BUSINESS_TERM")) + .thenReturn(1); + + service.updateTableAnnotation("game-users", "business_term", " "); + + verify(mapper).dropTableAnnotation("SGMP_POC", "CZN_COMN_USER_MST", "BUSINESS_TERM"); + verify(mapper, never()).addTableAnnotation(anyString(), anyString(), anyString(), anyString()); + } + + @Test + void rejectsAColumnThatIsNotPresentInTheSelectedTable() { + when(mapper.countColumn("SGMP_POC", "CZN_COMN_USER_MST", "UNKNOWN_COL")).thenReturn(0); + + assertThatThrownBy(() -> service.updateColumnComment("game-users", "unknown_col", "설명")) + .isInstanceOf(AppException.class) + .hasMessage("선택한 테이블에 존재하지 않는 컬럼입니다."); + } +}