/** * 십자말풀이 스테이지 관리 도구 - Google Apps Script */ function onOpen() { const ui = SpreadsheetApp.getUi(); ui.createMenu('🧩 십자말풀이 관리') .addItem('1. stages.json 생성 및 검증', 'exportStagesJson') .addSeparator() .addItem('2. 샘플 데이터 입력', 'populateSampleData') .addToUi(); } /** * 시트 데이터를 읽어 검증 후 JSON 모달 창을 띄웁니다. */ function exportStagesJson() { const sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet(); const data = sheet.getDataRange().getValues(); if (data.length < 2) { SpreadsheetApp.getUi().alert('데이터가 없습니다. 먼저 스테이지 데이터를 입력해 주세요.'); return; } const stagesMap = new Map(); const errors = []; // 2행부터 데이터 파싱 for (let i = 1; i < data.length; i++) { const row = data[i]; const rowNum = i + 1; const stageNumber = parseInt(row[0]); if (isNaN(stageNumber)) continue; const title = String(row[1] || '').trim(); const gridSize = parseInt(row[2]) || 6; const difficulty = parseInt(row[3]) || 1; const clueId = String(row[4] || `c${stageNumber}_${row[5]}`).trim(); const number = parseInt(row[5]); const direction = String(row[6] || 'ACROSS').trim().toUpperCase(); const word = String(row[7] || '').trim().replace(/\s+/g, ''); const clue = String(row[8] || '').trim(); const startRow = parseInt(row[9]); const startCol = parseInt(row[10]); const length = parseInt(row[11]) || word.length; // 1. 유효성 검사 if (word.length !== length) { errors.push(`[${rowNum}행] '${word}'의 글자 수(${word.length})와 length(${length})가 불일치합니다.`); } if (direction === 'ACROSS') { if (startCol + length > gridSize) { errors.push(`[${rowNum}행 Stage ${stageNumber}] '${word}' 가로 단어가 gridSize(${gridSize}) 범위를 벗어납니다. (startCol: ${startCol} + length: ${length} = ${startCol + length})`); } } else if (direction === 'DOWN') { if (startRow + length > gridSize) { errors.push(`[${rowNum}행 Stage ${stageNumber}] '${word}' 세로 단어가 gridSize(${gridSize}) 범위를 벗어납니다. (startRow: ${startRow} + length: ${length} = ${startRow + length})`); } } if (!stagesMap.has(stageNumber)) { stagesMap.set(stageNumber, { stageNumber: stageNumber, title: title, gridSize: gridSize, difficulty: difficulty, clues: [] }); } stagesMap.get(stageNumber).clues.push({ id: clueId, number: number, direction: direction, word: word, clue: clue, startRow: startRow, startCol: startCol, length: length }); } // 2. 스테이지별 교차점 글자 충돌 검사 stagesMap.forEach((stage, stageNum) => { const gridMap = new Map(); stage.clues.forEach(clue => { for (let idx = 0; idx < clue.word.length; idx++) { const r = clue.direction === 'ACROSS' ? clue.startRow : clue.startRow + idx; const c = clue.direction === 'ACROSS' ? clue.startCol + idx : clue.startCol; const key = `${r},${c}`; const char = clue.word[idx]; if (gridMap.has(key)) { const existing = gridMap.get(key); if (existing.char !== char) { errors.push(`[Stage ${stageNum}] 교차점 (${r}, ${c}) 글자 충돌: '${existing.word}'의 '${existing.char}' vs '${clue.word}'의 '${char}'`); } } else { gridMap.set(key, { char: char, word: clue.word }); } } }); }); const stagesArray = Array.from(stagesMap.values()); const jsonString = JSON.stringify(stagesArray, null, 2); // 모달 팝업 표시 showJsonDialog(jsonString, errors); } /** * JSON 결과 및 복사 버튼 팝업 UI */ function showJsonDialog(jsonString, errors) { let errorHtml = ''; if (errors.length > 0) { errorHtml = `