Quy Nhơn Travel Guide
좋았던 장소를 공유해주세요!여행을 좋아하는 사람 중에 나쁜 사람은 없더라고요.
꾸이년까지 온 당신도 그런 사람이겠죠.
받은 만큼 남긴 한 줄이, 내 다음 여행도 행복하게 만듭니다.
MAP
마커를 누르면 구글맵으로 바로 연결돼요. 지도가 안 보이면 배포된 사이트 링크에서 확인해주세요 (미리보기 환경에서는 지도 타일이 제한될 수 있어요).
카테고리별 CSV를 내려받아 Google My Maps에서 가져오기(Import) 하면 장소 전체가 한 번에 내 지도에 찍혀요.
🔒 구글맵 저장은 회원 전용이에요 — 카카오로 3초 가입하면 149곳 전체를 내 지도에 담을 수 있어요.
나만 아는 꾸이년 장소가 있나요? 구글맵 링크와 상호명만 적으면 목록 맨 위에 바로 올라가요. 닉네임을 남기면 카드에 🙌 추천자로 함께 표시돼요.
이 설정은 사장님만 볼 수 있어요. 비밀번호를 입력해주세요.
📝 주인장 3줄평 연동
구글 시트를 하나 만들어 첫 행에 장소명 / 리뷰 / 네이버링크 / 사진링크 / 구글맵주소 다섯 열 헤더를 넣어주세요 (순서는 상관없어요, 열 이름만 저 단어를 포함하면 돼요). 아래 "3줄평 시트에 전체 장소명 동기화" 버튼을 누르면 장소명과 구글맵주소 칸이 자동으로 채워져서, 사장님은 리뷰·네이버링크·사진링크만 적으시면 돼요 — 방문자가 넣은 사진이 있어도 사진링크에 뭔가 적어두면 그 사진이 항상 대표 사진으로 고정돼요. 다 설정한 뒤 시트에서 파일 → 공유 → 웹에 게시로 들어가 형식을 쉼표로 구분된 값(.csv)으로 바꿔 게시하고, 생성된 링크를 아래에 붙여넣으면 시트에 적은 3줄평·네이버 블로그 링크·사진이 이 페이지에 자동으로 반영돼요.
설정 패널을 열 때마다, 그리고 방문자가 새 장소를 추가할 때마다 자동으로 동기화돼요. 지금 바로 반영하고 싶으면 버튼을 눌러주세요.
💬 방문자 댓글·장소 제보 연동 (선택)
방문자가 남긴 댓글·좋아요·"장소 추가하기" 제보, 오늘의 방문자 수를 모두에게 보여주고 3줄평 시트에 장소명·구글맵주소를 자동으로 채워 넣으려면 같은 구글 시트에 탭을 다섯 개 만들어주세요:
댓글 탭(첫 행: 날짜 | 장소명 | 이름 | 댓글 | 사진링크), 제안장소 탭(첫 행: 날짜 | 상호명 | 카테고리 | 링크 | 메모 | 닉네임 | 사진링크), 방문기록 탭(첫 행: 날짜), 좋아요 탭(첫 행: 날짜 | 장소명 | 닉네임 | 액션),
그리고 3줄평 적어두신 탭 이름을 정확히 3줄평으로 맞춰주세요(첫 행에 장소명 열과 "구글맵주소"라는 단어가 들어간 열이 있어야 자동으로 채워질 위치를 찾아요 — 나머지 리뷰·네이버링크·사진링크 열은 순서 상관없어요).
그 다음 1) 아래 Apps Script 코드를 확장 프로그램 → Apps Script에 붙여넣고 배포 → 배포 관리 → 수정(연필) → 새 버전 → 배포(기존에 연동해두셨다면 URL은 그대로예요)로 재배포한 URL을 "저장 URL"에,
2) 네 탭을 각각 파일 → 공유 → 웹에 게시 → CSV로 게시한 링크를 "댓글 읽기", "제보 읽기", "방문기록 읽기", "좋아요 읽기"에 넣어주세요. (3줄평 탭은 이미 쓰시던 그 게시 링크 그대로 두시면 돼요)
좋아요는 방문자가 별도 로그인 없이 하트 버튼만 누르면 바로 기록되고(브라우저별로 저장돼요), "❤️ 좋아요한 곳만" 필터로 자기가 누른 곳만 모아볼 수 있어요. 좋아요가 많은 곳은 목록 위쪽으로 자동 정렬돼요.
설정하시면 아래 "3줄평 시트에 전체 장소명 동기화" 버튼으로 지금 있는 모든 장소를 한 번에 채워 넣을 수 있고, 그 이후로는 방문자가 새 장소를 추가할 때마다 자동으로 3줄평 시트에도 장소명·구글맵주소가 추가돼요 — 사장님은 리뷰·네이버링크·사진링크 칸만 채우시면 돼요.
🔐 카카오·네이버 로그인(회원제): 회원가입 기록·회원수 표시를 쓰려면 회원 탭(첫 행: 날짜 | provider | id | 닉네임)을 하나 더 만들고, 그 탭의 CSV 게시 링크를 "회원 읽기"에 넣어주세요. 카카오/네이버 로그인 키는 개발자 앱 등록 후 별도로 심어드려요(정식 사이트에서 작동). 로그인한 회원은 좋아요가 기기 상관없이 유지되고, 히어로에 "회원 N명"이 표시돼요.
function doPost(e) {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var d = JSON.parse(e.postData.contents);
if (d.type === 'place') {
var sh = ss.getSheetByName('제안장소') || ss.insertSheet('제안장소');
sh.appendRow([new Date(), d.name, d.category, d.link, d.memo, d.nick || '익명', d.photo || '']);
syncPlaceToReviewSheet(ss, d.name, d.link);
} else if (d.type === 'visit') {
var sh = ss.getSheetByName('방문기록') || ss.insertSheet('방문기록');
var stamp = Utilities.formatDate(new Date(), Session.getScriptTimeZone(), 'yyyy-MM-dd HH:mm:ss');
sh.appendRow(["'" + stamp]); // leading ' forces plain text so it isn't reformatted by sheet locale
} else if (d.type === 'sync_places') {
syncAllPlacesToReviewSheet(ss, d.items || []);
} else if (d.type === 'like') {
var sh = ss.getSheetByName('좋아요') || ss.insertSheet('좋아요');
var stamp = Utilities.formatDate(new Date(), Session.getScriptTimeZone(), 'yyyy-MM-dd HH:mm:ss');
sh.appendRow(["'" + stamp, d.place, d.nick, d.action]);
} else if (d.type === 'owner_review') {
updateOwnerReview(ss, d.place, d.review, d.naver, d.photo);
} else if (d.type === 'member') {
var sh = ss.getSheetByName('회원') || ss.insertSheet('회원');
var last = sh.getLastRow();
var foundRow = 0; // one row per (provider,id) so the count stays clean
if (last >= 2) {
var vals = sh.getRange(2, 2, last - 1, 2).getValues();
for (var k = 0; k < vals.length; k++) {
if (String(vals[k][0]) === String(d.provider) && String(vals[k][1]) === String(d.id)) { foundRow = k + 2; break; }
}
}
if (foundRow) {
sh.getRange(foundRow, 4).setValue(d.nickname); // keep the chosen nickname current
} else {
var stamp = Utilities.formatDate(new Date(), Session.getScriptTimeZone(), 'yyyy-MM-dd HH:mm:ss');
sh.appendRow(["'" + stamp, d.provider, d.id, d.nickname]);
}
} else {
var sh = ss.getSheetByName('댓글') || ss.insertSheet('댓글');
sh.appendRow([new Date(), d.place, d.name || '익명', d.comment, d.photo || '']);
}
return ContentService.createTextOutput(JSON.stringify({status:'ok'}))
.setMimeType(ContentService.MimeType.JSON);
}
// Upsert into the "3줄평" tab: fills 장소명/구글맵주소 by matching header text
// (order-independent), never touches 리뷰/네이버링크/사진링크 for existing rows.
function syncPlaceToReviewSheet(ss, name, mapUrl) {
var sh = ss.getSheetByName('3줄평');
if (!sh || !name) return;
var lastCol = Math.max(sh.getLastColumn(), 1);
var header = sh.getRange(1, 1, 1, lastCol).getValues()[0];
var nameCol = -1, addrCol = -1;
for (var i = 0; i < header.length; i++) {
var h = String(header[i] || '');
if (nameCol === -1 && h.indexOf('장소') !== -1) nameCol = i + 1;
if (addrCol === -1 && h.indexOf('구글맵') !== -1) addrCol = i + 1;
}
if (nameCol === -1) return;
var lastRow = sh.getLastRow();
var names = lastRow >= 2 ? sh.getRange(2, nameCol, lastRow - 1, 1).getValues().map(function(r){ return r[0]; }) : [];
var idx = names.indexOf(name);
if (idx === -1) {
var width = Math.max(header.length, nameCol, addrCol);
var newRow = [];
for (var c = 1; c <= width; c++) newRow.push('');
newRow[nameCol - 1] = name;
if (addrCol > 0) newRow[addrCol - 1] = mapUrl || '';
sh.appendRow(newRow);
} else if (addrCol > 0) {
var cell = sh.getRange(idx + 2, addrCol);
if (!String(cell.getValue()).trim()) cell.setValue(mapUrl || ''); // don't clobber a hand-picked link
}
}
// Owner edits made from the site: writes 리뷰/네이버링크/사진링크 into that place's
// row in "3줄평" (creating the row if it isn't there yet). 구글맵주소는 건드리지 않아요.
function updateOwnerReview(ss, name, review, naver, photo) {
var sh = ss.getSheetByName('3줄평');
if (!sh || !name) return;
var lastCol = Math.max(sh.getLastColumn(), 1);
var header = sh.getRange(1, 1, 1, lastCol).getValues()[0];
var nameCol = -1, revCol = -1, navCol = -1, phCol = -1;
for (var i = 0; i < header.length; i++) {
var h = String(header[i] || '');
if (nameCol === -1 && h.indexOf('장소') !== -1) nameCol = i + 1;
if (revCol === -1 && (h.indexOf('리뷰') !== -1 || h.indexOf('3줄') !== -1)) revCol = i + 1;
if (navCol === -1 && h.indexOf('네이버') !== -1) navCol = i + 1;
if (phCol === -1 && h.indexOf('사진') !== -1) phCol = i + 1;
}
if (nameCol === -1) return;
var lastRow = sh.getLastRow();
var names = lastRow >= 2 ? sh.getRange(2, nameCol, lastRow - 1, 1).getValues().map(function(r){ return String(r[0]).trim(); }) : [];
var idx = names.indexOf(String(name).trim());
var row;
if (idx === -1) {
var width = Math.max(header.length, nameCol, revCol, navCol, phCol);
var newRow = [];
for (var c = 1; c <= width; c++) newRow.push('');
newRow[nameCol - 1] = name;
sh.appendRow(newRow);
row = sh.getLastRow();
} else {
row = idx + 2;
}
if (revCol > 0) sh.getRange(row, revCol).setValue(review || '');
if (navCol > 0) sh.getRange(row, navCol).setValue(naver || '');
if (phCol > 0) sh.getRange(row, phCol).setValue(photo || '');
}
// Batch version for the "전체 장소명 동기화" button: reads the sheet once and
// appends every missing place in ONE write (fast + reliable for all 149 at
// once), preserving the order they were sent. Existing rows keep 리뷰/네이버링크/
// 사진링크 untouched, and a 구글맵주소 you already typed is never overwritten —
// only empty 구글맵주소 cells get auto-filled.
function syncAllPlacesToReviewSheet(ss, items) {
var sh = ss.getSheetByName('3줄평');
if (!sh || !items.length) return;
var lastCol = Math.max(sh.getLastColumn(), 1);
var header = sh.getRange(1, 1, 1, lastCol).getValues()[0];
var nameCol = -1, addrCol = -1;
for (var i = 0; i < header.length; i++) {
var h = String(header[i] || '');
if (nameCol === -1 && h.indexOf('장소') !== -1) nameCol = i + 1;
if (addrCol === -1 && h.indexOf('구글맵') !== -1) addrCol = i + 1;
}
if (nameCol === -1) return;
var width = Math.max(header.length, nameCol, addrCol);
var lastRow = sh.getLastRow();
var existing = [], existingAddr = [];
if (lastRow >= 2) {
existing = sh.getRange(2, nameCol, lastRow - 1, 1).getValues().map(function(r){ return String(r[0]).trim(); });
if (addrCol > 0) existingAddr = sh.getRange(2, addrCol, lastRow - 1, 1).getValues().map(function(r){ return String(r[0]).trim(); });
}
var newRows = [];
for (var j = 0; j < items.length; j++) {
var name = String(items[j].name || '').trim();
if (!name) continue;
var mapUrl = items[j].mapUrl || '';
var idx = existing.indexOf(name);
if (idx === -1) {
var row = [];
for (var c = 1; c <= width; c++) row.push('');
row[nameCol - 1] = name;
if (addrCol > 0) row[addrCol - 1] = mapUrl;
newRows.push(row);
existing.push(name); // guard against duplicates within the same batch
} else if (addrCol > 0 && !existingAddr[idx]) {
sh.getRange(idx + 2, addrCol).setValue(mapUrl); // only fill an empty cell
}
}
if (newRows.length) {
sh.getRange(sh.getLastRow() + 1, 1, newRows.length, width).setValues(newRows);
}
}
꾸이년 여행자 회원가입
카카오로 3초 가입 👋
좋아요·구글맵 저장이 내 것이 되고,
꾸이년 여행자 커뮤니티 회원이 돼요.
🙂 닉네임을 정해주세요
카카오 이름이 실명이면 부담스러우시죠. 사이트에는 여기서 정한 닉네임만 보이고, 실명은 어디에도 표시되지 않아요. 언제든 바꿀 수 있어요.
💬 꾸이년 여행자 채팅
질문 · 번개모임 · 실시간 정보
아직 대화가 없어요.
첫 인사를 남겨보세요 👋
채팅은 회원만 참여할 수 있어요.
카카오로 3초 가입하고 대화에 참여해보세요!
✏️ 주인장 편집
저장하면 구글시트 3줄평 탭에도 자동 반영돼요.