first commit
This commit is contained in:
@@ -0,0 +1,215 @@
|
||||
/**
|
||||
* 십자말풀이 스테이지 관리 도구 - 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 = `
|
||||
<div style="background:#FFEBEE; border:1px solid #EF5350; padding:10px; border-radius:6px; margin-bottom:12px; color:#C62828;">
|
||||
<b>⚠️ 검증 오류 발견 (${errors.length}건):</b><br/>
|
||||
<ul style="margin:5px 0 0 15px; padding:0; font-size:12px;">
|
||||
${errors.map(e => `<li>${e}</li>`).join('')}
|
||||
</ul>
|
||||
</div>
|
||||
`;
|
||||
} else {
|
||||
errorHtml = `
|
||||
<div style="background:#E8F5E9; border:1px solid #66BB6A; padding:10px; border-radius:6px; margin-bottom:12px; color:#2E7D32;">
|
||||
<b>✅ 모든 데이터 및 교차점 검증 완료 (오류 없음)</b>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
const html = `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<base target="_top">
|
||||
<style>
|
||||
body { font-family: sans-serif; margin: 15px; }
|
||||
textarea { width: 100%; height: 350px; font-family: monospace; font-size: 11px; padding: 8px; box-sizing: border-box; }
|
||||
.btn { background: #4F46E5; color: white; border: none; padding: 8px 16px; border-radius: 6px; cursor: pointer; font-weight: bold; }
|
||||
.btn:hover { background: #4338CA; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
${errorHtml}
|
||||
<textarea id="jsonArea" readonly>${jsonString}</textarea>
|
||||
<div style="margin-top: 10px; display: flex; justify-content: space-between; align-items: center;">
|
||||
<button class="btn" onclick="copyJson()">📋 클립보드에 전체 복사</button>
|
||||
<span id="copyMsg" style="color: #4F46E5; font-size: 12px; font-weight: bold;"></span>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function copyJson() {
|
||||
const copyText = document.getElementById("jsonArea");
|
||||
copyText.select();
|
||||
document.execCommand("copy");
|
||||
document.getElementById("copyMsg").innerText = "복사되었습니다! Firebase 또는 파일에 붙여넣으세요.";
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
|
||||
const htmlOutput = HtmlService.createHtmlOutput(html)
|
||||
.setWidth(680)
|
||||
.setHeight(520);
|
||||
SpreadsheetApp.getUi().showModalDialog(htmlOutput, 'stages.json 내보내기');
|
||||
}
|
||||
|
||||
/**
|
||||
* 1~4 스테이지 샘플 데이터를 시트에 자동으로 채워줍니다.
|
||||
*/
|
||||
function populateSampleData() {
|
||||
const sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
|
||||
sheet.clear();
|
||||
|
||||
const headers = [
|
||||
'stageNumber', 'title', 'gridSize', 'difficulty',
|
||||
'clueId', 'number', 'direction', 'word', 'clue', 'startRow', 'startCol', 'length'
|
||||
];
|
||||
|
||||
const sampleRows = [
|
||||
[1, '첫걸음 (자연과 일상)', 6, 1, 'c1_1', 1, 'ACROSS', '가을하늘', '가을철의 맑고 푸른 하늘.', 1, 0, 4],
|
||||
[1, '첫걸음 (자연과 일상)', 6, 1, 'c1_2', 2, 'DOWN', '하늘나라', '천국을 이르는 말.', 1, 2, 4],
|
||||
[1, '첫걸음 (자연과 일상)', 6, 1, 'c1_3', 3, 'ACROSS', '나비효과', '미세한 변화가 엄청난 결과를 낳는 현상.', 3, 2, 4],
|
||||
|
||||
[2, '동네 한바퀴', 6, 1, 'c2_1', 1, 'ACROSS', '과일가게', '여러 가지 과일을 파는 상점.', 0, 1, 4],
|
||||
[2, '동네 한바퀴', 6, 1, 'c2_2', 2, 'DOWN', '가면무도회', '참가자가 가면을 쓰고 춤을 추는 연회.', 0, 3, 5],
|
||||
[2, '동네 한바퀴', 6, 1, 'c2_3', 3, 'ACROSS', '무지개', '비 갠 하늘에 나타나는 일곱 빛깔 호선.', 2, 3, 3],
|
||||
[2, '동네 한바퀴', 6, 1, 'c2_4', 4, 'DOWN', '개구쟁이', '궂은 장난을 자주 치는 귀여운 아이.', 2, 5, 4],
|
||||
|
||||
[3, '사자성어와 지혜', 6, 2, 'c3_1', 1, 'ACROSS', '일석이조', '돌 하나로 새 두 마리를 잡는다는 사자성어.', 1, 0, 4],
|
||||
[3, '사자성어와 지혜', 6, 2, 'c3_2', 2, 'DOWN', '조약돌', '물에 씻기어 둥글고 반드러워진 작은 돌.', 1, 3, 3],
|
||||
[3, '사자성어와 지혜', 6, 2, 'c3_3', 3, 'ACROSS', '돌다리', '돌로 놓은 다리. 신중함을 뜻하는 속담 소재.', 3, 3, 3],
|
||||
[3, '사자성어와 지혜', 6, 2, 'c3_4', 4, 'DOWN', '다락방', '지붕 바로 밑이나 천장 위에 꾸며 놓은 방.', 3, 4, 3],
|
||||
|
||||
[4, '문화와 예술', 6, 2, 'c4_1', 1, 'ACROSS', '방방곡곡', '전국 방방곡곡, 한 곳도 빠짐없는 모든 곳.', 1, 0, 4],
|
||||
[4, '문화와 예술', 6, 2, 'c4_2', 2, 'DOWN', '곡예사', '서커스나 줄타기 따위 곡예를 하는 사람.', 1, 3, 3],
|
||||
[4, '문화와 예술', 6, 2, 'c4_3', 3, 'ACROSS', '고사성어', '옛날 이야기나 고사에서 유래된 사자성어 및 숙어.', 3, 2, 4],
|
||||
[4, '문화와 예술', 6, 2, 'c4_4', 4, 'DOWN', '어린이', '어린아이를 대접하여 부르는 말.', 3, 5, 3],
|
||||
[4, '문화와 예술', 6, 2, 'c4_5', 5, 'ACROSS', '호랑이', '고양이과의 사나운 맹수, 산군.', 5, 3, 3]
|
||||
];
|
||||
|
||||
sheet.getRange(1, 1, 1, headers.length).setValues([headers]).setFontWeight('bold').setBackground('#E0E7FF');
|
||||
sheet.getRange(2, 1, sampleRows.length, headers.length).setValues(sampleRows);
|
||||
sheet.autoResizeColumns(1, headers.length);
|
||||
|
||||
SpreadsheetApp.getUi().alert('샘플 데이터가 생성되었습니다!');
|
||||
}
|
||||
Reference in New Issue
Block a user