diff --git a/feedback/index.html b/feedback/index.html new file mode 100644 index 0000000..94e1832 --- /dev/null +++ b/feedback/index.html @@ -0,0 +1,296 @@ + + + + + + 의견 게시판 · 널스메이트 + + + + + + + + + +
+
+
+ + + + +
+
+ + + + diff --git a/index.html b/index.html index 6f4676d..13b85cb 100644 --- a/index.html +++ b/index.html @@ -58,9 +58,9 @@

간호사분들의
더 나은 하루를 위해

03 / 워치 연동

주머니 속에서
손목 위로.

휴대폰을 꺼내기 어려운 순간에도,
손목에서 처치 타이머를 확인하세요.

휴대폰 + 스마트워치
워치 처치 타이머 목록 화면 예시
워치 타이머 상세 화면 예시
-

궁금한 점이 있나요?

자주 묻는 질문

도움이 더 필요하다면
어떤 기기에서 사용할 수 있나요?

iOS는 App Store에서, Android는 Google Play에서 다운로드할 수 있어요. iOS 앱은 iOS 17 이상, iOS의 처치 타이머는 iOS 26.1 이상에서 이용할 수 있습니다.

알약 식별 결과만으로 투약해도 되나요?

알약 식별 결과는 참고용 보조 정보예요. 투약 전 반드시 처방 내용과 약품 라벨을 확인하고, 최종 판단은 의료진의 확인을 따라 주세요.

로그인이 필요한가요?

네, 널스메이트는 로그인 후 이용할 수 있어요. Google, Apple, 카카오 계정으로 시작할 수 있습니다.

문의나 의견은 어디로 보내나요?

지원 페이지를 방문하거나 2park1bae@gmail.com으로 보내주세요. 현장의 의견을 기다리고 있어요.

+

궁금한 점이 있나요?

자주 묻는 질문

도움이 더 필요하다면
어떤 기기에서 사용할 수 있나요?

iOS는 App Store에서, Android는 Google Play에서 다운로드할 수 있어요. iOS 앱은 iOS 17 이상, iOS의 처치 타이머는 iOS 26.1 이상에서 이용할 수 있습니다.

알약 식별 결과만으로 투약해도 되나요?

알약 식별 결과는 참고용 보조 정보예요. 투약 전 반드시 처방 내용과 약품 라벨을 확인하고, 최종 판단은 의료진의 확인을 따라 주세요.

로그인이 필요한가요?

네, 널스메이트는 로그인 후 이용할 수 있어요. Google, Apple, 카카오 계정으로 시작할 수 있습니다.

문의나 의견은 어디로 보내나요?

의견은 의견 게시판에 남겨 주세요. 문의는 지원 페이지를 방문하거나 2park1bae@gmail.com으로 보내주세요. 현장의 의견을 기다리고 있어요.

널스메이트 앱 아이콘

오늘도 애쓰는 당신에게

다음 근무부터,
널스메이트와 함께해요.

널스메이트 다운로드 페이지로 연결되는 QR 코드카메라로 QR을 스캔해 보세요

휴대폰에 맞는 다운로드 안내로 연결돼요.

다운로드 페이지 열기
- + diff --git a/scripts/build-pages.py b/scripts/build-pages.py index 2c16294..d489813 100644 --- a/scripts/build-pages.py +++ b/scripts/build-pages.py @@ -10,7 +10,7 @@ "index.html", "404.html", "styles.css", "landing.css", "CNAME", ".nojekyll", "robots.txt", "sitemap.xml", ) -SITE_DIRECTORIES = ("assets", "terms", "privacy", "support", "download") +SITE_DIRECTORIES = ("assets", "terms", "privacy", "support", "download", "feedback") def copy_site(source, target): diff --git a/scripts/feedback/Code.gs b/scripts/feedback/Code.gs new file mode 100644 index 0000000..de6e354 --- /dev/null +++ b/scripts/feedback/Code.gs @@ -0,0 +1,146 @@ +/** + * @OnlyCurrentDoc + * 위 표시로 이 스크립트는 붙여 넣은 스프레드시트 하나에만 접근 권한을 요청한다 (드라이브의 다른 파일은 못 건드림). + */ + +/** + * 널스메이트 의견 게시판 서버 (Google Apps Script 웹 앱) + * + * 붙여 넣을 스프레드시트의 "글" 탭에 의견을 저장하고, 공개된 글과 운영자 답변을 돌려준다. + * 운영자는 시트에서 직접 관리한다. + * - 답변: "답변" 칸에 적으면 페이지에 "널스메이트 팀" 답변으로 보인다. 답변시각은 자동 기록. + * - 숨김: "공개" 칸을 N으로 바꾸면 페이지에서 사라진다. + */ + +const SHEET_NAME = '글'; +const HEADERS = ['ID', '작성시각', '닉네임', '내용', '답변', '답변시각', '공개']; +const COL = { id: 1, createdAt: 2, nickname: 3, content: 4, reply: 5, repliedAt: 6, visible: 7 }; + +// true면 새 글이 "공개" N으로 저장되어, 운영자가 Y로 바꿔야 보인다. +const REQUIRE_APPROVAL = false; +const MAX_CONTENT = 1000; +const MAX_NICKNAME = 20; +const MAX_LIST = 200; +// 브라우저(작성자)별 제한. Apps Script는 IP를 알 수 없어 페이지가 브라우저에 저장한 ID로 구분한다. +const COOLDOWN_SECONDS = 30; +const POSTS_PER_HOUR = 5; +// 운영팀 사칭 방지: 공백·대소문자 무시하고 이 단어가 들어간 닉네임은 받지 않는다. +const RESERVED_NICKNAMES = ['널스메이트', 'nursemate', '운영자', '운영팀', '관리자', 'admin', '투박한배', 'twoparkonebae']; + +function doGet() { + const sheet = getSheet_(); + const rows = sheet.getLastRow() > 1 + ? sheet.getRange(2, 1, sheet.getLastRow() - 1, HEADERS.length).getValues() + : []; + const posts = rows + .filter(function (r) { return String(r[COL.visible - 1]).trim().toUpperCase() !== 'N' && r[COL.content - 1] !== ''; }) + .map(function (r) { + return { + id: String(r[COL.id - 1]), + createdAt: toIso_(r[COL.createdAt - 1]), + nickname: String(r[COL.nickname - 1] || ''), + content: String(r[COL.content - 1]), + reply: String(r[COL.reply - 1] || ''), + repliedAt: toIso_(r[COL.repliedAt - 1]), + }; + }) + .reverse() + .slice(0, MAX_LIST); + return json_({ ok: true, posts: posts }); +} + +function doPost(e) { + let body; + try { + body = JSON.parse(e.postData.contents); + } catch (err) { + return json_({ ok: false, error: 'invalid' }); + } + // 사람에게 보이지 않는 칸이 채워져 있으면 봇으로 보고 성공처럼 응답만 한다. + if (body.website) return json_({ ok: true }); + + const content = String(body.content || '').trim(); + const nickname = String(body.nickname || '').trim().slice(0, MAX_NICKNAME); + const client = String(body.client || ''); + if (!/^[A-Za-z0-9-]{16,64}$/.test(client)) return json_({ ok: false, error: 'invalid' }); + if (!content) return json_({ ok: false, error: 'empty' }); + if (isReservedNickname_(nickname)) return json_({ ok: false, error: 'reserved_nickname' }); + if (content.length > MAX_CONTENT) return json_({ ok: false, error: 'too_long' }); + + const lock = LockService.getScriptLock(); + if (!lock.tryLock(10000)) return json_({ ok: false, error: 'busy' }); + try { + const limited = takeRateSlot_(client); + if (limited) return json_({ ok: false, error: limited }); + getSheet_().appendRow([ + Utilities.getUuid().slice(0, 8), + new Date(), + safeCell_(nickname), + safeCell_(content), + '', + '', + REQUIRE_APPROVAL ? 'N' : 'Y', + ]); + } finally { + lock.releaseLock(); + } + return json_({ ok: true, pending: REQUIRE_APPROVAL }); +} + +// 운영자가 "답변" 칸을 고치면 답변시각을 채운다 (설치 없이 동작하는 단순 트리거). +function onEdit(e) { + const range = e.range; + if (range.getSheet().getName() !== SHEET_NAME || range.getRow() < 2) return; + if (range.getColumn() > COL.reply || range.getLastColumn() < COL.reply) return; + for (let row = range.getRow(); row <= range.getLastRow(); row++) { + const sheet = range.getSheet(); + const reply = sheet.getRange(row, COL.reply).getValue(); + sheet.getRange(row, COL.repliedAt).setValue(reply === '' ? '' : new Date()); + } +} + +function getSheet_() { + const ss = SpreadsheetApp.getActiveSpreadsheet(); + let sheet = ss.getSheetByName(SHEET_NAME); + if (!sheet) { + sheet = ss.insertSheet(SHEET_NAME); + sheet.appendRow(HEADERS); + sheet.setFrozenRows(1); + sheet.getRange(1, 1, 1, HEADERS.length).setFontWeight('bold'); + sheet.setColumnWidth(COL.content, 420); + sheet.setColumnWidth(COL.reply, 320); + } + return sheet; +} + +// 제한에 걸리면 오류 코드를, 통과하면 null을 돌려준다. +function takeRateSlot_(client) { + const cache = CacheService.getScriptCache(); + const cooldownKey = 'cooldown:' + client; + const hourKey = 'hour:' + client + ':' + Math.floor(Date.now() / 3600000); + if (cache.get(cooldownKey)) return 'too_fast'; + const count = Number(cache.get(hourKey) || 0); + if (count >= POSTS_PER_HOUR) return 'rate_limited'; + cache.put(cooldownKey, '1', COOLDOWN_SECONDS); + cache.put(hourKey, String(count + 1), 3700); + return null; +} + +function isReservedNickname_(nickname) { + const normalized = nickname.replace(/\s+/g, '').toLowerCase(); + return RESERVED_NICKNAMES.some(function (word) { return normalized.indexOf(word) !== -1; }); +} + +// =, +, -, @ 로 시작하는 입력이 시트에서 수식으로 실행되지 않게 막는다. +function safeCell_(value) { + return /^[=+\-@]/.test(value) ? "'" + value : value; +} + +function toIso_(value) { + return value instanceof Date ? value.toISOString() : ''; +} + +function json_(data) { + return ContentService.createTextOutput(JSON.stringify(data)) + .setMimeType(ContentService.MimeType.JSON); +} diff --git a/scripts/feedback/README.md b/scripts/feedback/README.md new file mode 100644 index 0000000..68278c2 --- /dev/null +++ b/scripts/feedback/README.md @@ -0,0 +1,22 @@ +# 의견 게시판 서버 설정 + +`/feedback/` 페이지는 구글 스프레드시트 + Apps Script 웹 앱에 글을 저장한다. 별도 서버가 없다. +`scripts/`는 배포 산출물에 포함되지 않으므로 `Code.gs`는 공개 사이트에 올라가지 않는다. + +## 처음 한 번 + +1. 운영자 구글 계정의 드라이브에서 새 스프레드시트를 만든다 (예: `널스메이트 의견 게시판`). +2. **확장 프로그램 → Apps Script**를 열고 `Code.gs` 내용을 통째로 붙여 넣은 뒤 저장한다. +3. **배포 → 새 배포** → 유형 **웹 앱**, 실행 **나**, 액세스 **모든 사용자** → 배포. 처음엔 권한 승인 창이 뜨는데 허용한다. +4. 나온 `https://script.google.com/macros/s/.../exec` 주소를 `feedback/index.html`의 `const API = ""`에 넣는다. +5. 페이지에서 글을 하나 써 보면 시트에 `글` 탭이 생기고 그 글이 들어간다. + +## 운영 + +| 칸 | 용도 | +| --- | --- | +| 답변 | 적으면 페이지에 "널스메이트 팀" 답변으로 보인다. 답변시각은 자동 기록 | +| 공개 | `N`이면 페이지에서 숨긴다. 부적절한 글은 행을 지우지 말고 `N`으로 | + +- 승인 후 공개로 바꾸려면 `Code.gs`의 `REQUIRE_APPROVAL`을 `true`로. +- `Code.gs`를 고친 뒤에는 **배포 → 배포 관리 → 수정 → 새 버전**으로 다시 배포해야 반영된다. 새 배포를 만들면 주소가 바뀌니 기존 배포를 수정한다. diff --git a/sitemap.xml b/sitemap.xml index d302842..55d9745 100644 --- a/sitemap.xml +++ b/sitemap.xml @@ -2,6 +2,7 @@ https://www.nursemate.app/ https://www.nursemate.app/support/ + https://www.nursemate.app/feedback/ https://www.nursemate.app/terms/ https://www.nursemate.app/privacy/ diff --git a/support/index.html b/support/index.html index 7f446ad..dc9baa0 100644 --- a/support/index.html +++ b/support/index.html @@ -24,6 +24,9 @@ .support-contact-action { display:flex; align-items:center; flex-direction:column; gap:9px; flex-shrink:0; } .support-contact-action .button { padding:13px 21px; font-size:15px; gap:18px; border-radius:12px; } .support-email { font-size:12px; color:#527086; text-decoration:underline; text-underline-offset:3px; } + .support-feedback { margin-top:16px; color:#527086; font-size:14px; } + .support-feedback a { color:var(--blue); font-weight:700; } + .support-feedback a:hover { text-decoration:underline; } .support-main { padding-top:62px; padding-bottom:72px; } .support-faq-section { display:grid; grid-template-columns:240px minmax(0,1fr); gap:48px; } .support-section-title { font-size:27px; line-height:1.35; } @@ -104,6 +107,7 @@

무엇을 도와드릴까요?

도움이 필요하면 메일로 문의해 주세요

영업일 기준 2~3일 이내에 답변드리도록 노력하겠습니다.

메일로 문의하기 2park1bae@gmail.com
+

기능 제안이나 사용 후기는 의견 게시판 에 남겨 주셔도 좋아요.

@@ -130,6 +134,6 @@

무엇을 도와드릴까요?

최종 수정일: 2026-10-06

- +