// 그란데클립 메일 서명 만들기
// [보안] 이름 + 소속 + 생년월일 → Apps Script 인증 → "본인 레코드 1건"만 수신 → 서명 미리보기
//        → 미리보기 DOM의 HTML을 클립보드로 복사.
//        구성원 PII는 더 이상 정적 파일(members.json)로 공개하지 않는다. 데이터는 Apps Script 뒤에 있다.
//        (apps-script/Code.gs 를 웹 앱으로 배포한 뒤 그 URL을 아래 AUTH_API_URL 에 입력)
//
// HTML 텍스트 서명 방식: 미리보기 자체가 인라인 스타일로 그린 서명이고, 복사 시 그 DOM의 outerHTML을
// text/html로 클립보드에 넣는다. Gmail 서명란에 붙여넣으면 실제 텍스트 + 로고가 들어가 어느 해상도에서도
// 선명하고, 메일/전화는 클릭 링크가 된다.
//   왜 이미지(PNG) 방식을 버렸나: Gmail은 붙여넣은 이미지를 data URL로 서명에 통째로 저장 →
//   해상도를 올리면 글자수가 폭증해 "서명이 너무 길다" 한도 초과, 줄이면 흐려짐. 둘을 동시에 만족 불가.
//   대신 로고만 작게 최적화해 넣는다 (FnB·봉앤설은 inline data URL, 클립은 URL 참조 — 아래 참고).

// 로고: 소속별로 다름 — GCK=메탈 클립, 봉앤설=컬러 배너, FnB=grandeclip FnB 워드마크. (build-logo.py 생성)
//
// 클립만 인라인이 아니라 URL 참조다. GCK 표시 폭이 300px이라 레티나(600px) 소스가 필요한데,
// 그걸 data URL로 박으면 base64 23,000자 — Gmail 서명 1만자 한도를 2배 넘긴다.
// 그래서 클립은 이 앱이 이미 떠 있는 Vercel 도메인에서 그냥 이미지로 서빙한다.
//   - 절대 URL이어야 한다. 상대경로면 Gmail 서명에 붙여넣었을 때 깨진다.
//   - localhost에서도 이 prod URL을 본다 — 미리보기와 실제 복사본이 어긋나지 않게 하려는 의도.
//   - 파일명의 -600은 소스 폭. vercel.json이 /assets/*를 immutable 캐시로 걸어둬서 같은 이름으로
//     덮으면 옛 이미지가 계속 나온다. 해상도를 바꾸면 파일명도 같이 바꿀 것.
// FnB·봉앤설은 표시 폭이 작아(205/200px) 인라인 data URL로도 레티나 화질이 한도 안에 들어온다.
const LOGO_CLIP    = 'https://grande-mail-signature.vercel.app/assets/clip-600.png';   // 표시 300px × 2
const LOGO_FNB     = (typeof window !== 'undefined' && window.FNB_LOGO)     || '';
const LOGO_BONGSUL = (typeof window !== 'undefined' && window.BONGSUL_LOGO) || '';  // 봉앤설 컬러 배너(파랑 바탕). 맨 아래 배치.
const DEFAULT_LOGO_WIDTH = 140; // 표시 폭 (레티나 대비 소스는 더 큼)

// ── 인증 설정 ───────────────────────────────────────────────────────────────
// Apps Script 웹 앱 URL(.../exec). apps-script/Code.gs 배포 후 여기에 붙여넣는다.
// 비밀값 아님(엔드포인트). 인증은 서버(Apps Script)가 강제하므로 공개돼도 무방.
// 비어 있으면 로그인 화면이 "설정 대기" 안내를 표시한다.
const AUTH_API_URL = 'https://script.google.com/macros/s/AKfycbxt40wbiELCy0VgxsFaZgqnRwI3grhkYsf4UiNvq93AAvPrzUPc2hnSCnc4L9UsJlYSVQ/exec';

const SESSION_KEY = 'grandeclip.signature.session.v1';  // 로그인 세션(본인 레코드 + 만료시각)
const SESSION_TTL_MS = 12 * 60 * 60 * 1000;             // 세션 유지 12시간(본인 기기 한정)

// 주소 회사명에 거는 지도 링크. GCK·봉앤설은 본사(가로수길 14-4) 공통.
// FnB(종로 창덕궁1길)는 지도 링크 미정 → COMPANY_BY_AFFILIATION에서 빈 값(링크 없이 밑줄만).
const MAPS_URL_GAROSU = 'https://maps.app.goo.gl/ML7duensZqm4wrty9';

// 활성 소속(서명 발급 대상). AMCR·스테이폴리오는 마스터에 등록되지 않으므로 제외.
// value = 마스터시트 affiliation 값 (매칭 키), label = 풀네임 노출 텍스트.
const AFFILIATION_OPTIONS = [
  { value: 'GCK',    label: '그란데클립코리아' },
  { value: '봉앤설', label: '봉앤설이니셔티브' },
  { value: 'FnB',    label: '그란데클립FnB코리아' },
];

// 소속 → 회사 표기·주소(한/영) 매핑.
// 본사 주소 = 가로수길 14-4 (메모리 reference_grandeclip_office).
const COMPANY_BY_AFFILIATION = {
  'GCK': {
    nameEn: 'grandeclip',
    addressKr: '서울특별시 강남구 가로수길 14-4 (우.06036)',
    addressEn: 'grandeclip, 14-4, Garosu-gil, Gangnam-gu, Seoul, Korea, 06036',
    logo: LOGO_CLIP, logoWidth: 300, mapsUrl: 'https://naver.me/GdTTVdKp',   // 주소 하이퍼링크 (클립 폭 300px — 봉진님 기존 서명 크기 복원, 2026-07-22)
    logoLink: 'https://team.grandeclip.com/',   // 클립 이미지 클릭 시 열리는 링크
    contactGap: 1,  // 전화·주소·영문주소 줄간격 살짝 좁힘(기본 LINE_GAP=3). GCK만.
    defaultTeam: 'GVC',  // 팀 데이터 없으면 GVC로
    barFormat: true,     // 직책 양식: "직급, 팀｜grandeclip" (of 안 씀). 직책표기 있으면 "직책표기｜grandeclip"
    nameSize: 30, titleSize: 15, phoneSize: 15, addrSize: 14,  // 글씨 확대(본문 1.2배+이름 30px) — 큰 클립에 맞춰 가독성 ↑ (2026-07-22)
  },
  // 봉앤설은 별도 사양: 국문 직책 단독 표기(koreanTitle), 이메일 줄 노출(showEmail), 5층 주소, "Bong&Sul" 표기.
  // 로고는 GCK 메탈 클립과 다른 별도 이미지 — 사용자 제공 대기 중. 받으면 build-logo.py로 BONGSUL_LOGO 생성 후 교체.
  '봉앤설': {
    nameEn: 'Bong&Sul Initiative',
    addressKr: '서울특별시 강남구 가로수길 14-4 5층 봉앤설이니셔티브 (우.06036)',
    addressEn: 'Bong&Sul Initiative, 5F, 14-4, Garosu-gil, Gangnam-gu,\nSeoul, Korea, 06036',  // \n = 강제 줄바꿈
    logo: LOGO_BONGSUL, logoWidth: 200, mapsUrl: 'https://naver.me/FM99Rs7B',   // 주소 하이퍼링크
    // 직책은 GCK와 동일하게 직급 드롭다운+영문 직급으로(option B). 팀은 회사명과 중복이라 미표시(noTeam).
    // phoneAddrGap: 전화↔주소 사이 한 줄 띄움. contactGap: 이메일↔전화 간격 좁힘(2). (직급 뒤 회사명 표기)
    noTeam: true, showEmail: true, emailDomain: 'bongnsul.com', logoPosition: 'bottom',
    phoneAddrGap: 16, contactGap: 0,   // contactGap 0 → title↔email↔phone 간격이 주소 줄간격(line-height)과 동일
    logoLink: 'https://www.bongnsul.com/',   // 로고 클릭 시 열리는 링크
  },
  '봉앤설이니셔티브': {
    nameEn: 'Bong&Sul Initiative',
    addressKr: '서울특별시 강남구 가로수길 14-4 5층 봉앤설이니셔티브 (우.06036)',
    addressEn: 'Bong&Sul Initiative, 5F, 14-4, Garosu-gil, Gangnam-gu,\nSeoul, Korea, 06036',  // \n = 강제 줄바꿈
    logo: LOGO_BONGSUL, logoWidth: 200, mapsUrl: 'https://naver.me/FM99Rs7B',   // 주소 하이퍼링크
    // 직책은 GCK와 동일하게 직급 드롭다운+영문 직급으로(option B). 팀은 회사명과 중복이라 미표시(noTeam).
    // phoneAddrGap: 전화↔주소 사이 한 줄 띄움. contactGap: 이메일↔전화 간격 좁힘(2). (직급 뒤 회사명 표기)
    noTeam: true, showEmail: true, emailDomain: 'bongnsul.com', logoPosition: 'bottom',
    phoneAddrGap: 16, contactGap: 0,   // contactGap 0 → title↔email↔phone 간격이 주소 줄간격(line-height)과 동일
    logoLink: 'https://www.bongnsul.com/',   // 로고 클릭 시 열리는 링크
  },
  // FnB는 별도 사양(서명 세팅 가이드 슬라이드): grandeclip FnB 워드마크 로고, 종로 주소, 팀 미표기.
  'FnB': {
    nameEn: 'Grandeclip FnB Korea LLC.',
    addressKr: '서울 종로구 율곡로 82 4층 newmix',
    addressEn: 'Grandeclip FnB Korea LLC. 4F newmix, 82, Yulgok-ro, Jongno-gu, Seoul',  // 'newmix' 소문자 — 국문 표기와 통일
    logo: LOGO_FNB, logoWidth: 205, mapsUrl: 'https://naver.me/5dhhZDdc', noTeam: true,   // 주소 하이퍼링크
    // 타이포: 이름 17 / 직책 14·볼드·진한색 / 전화 볼드(색은 주소와 동일 회색). 로고는 직책줄 "FnB" 끝선까지(추정 205px).
    // 로고 왼쪽 여백만큼 텍스트 1칸 들여 정렬. 로고↔이름 간격 좁게(logoGap 4). 직책↔전화 간격 ↑(contactGap 6).
    // 전화·주소 3줄 간격은 동일하게 살짝 좁게(phoneAddrGap/addrGap -2).
    nameSize: 17, titleSize: 14, titleStrong: true, phoneBold: true, surnameUpper: true,
    nameTitleGap: 1, phoneSize: 12, addrSize: 11,   // 이름↔직책 좁게 / 하단 3줄 폰트 살짝 작게
    nameIndent: 1, contactIndent: 1, logoGap: 4, contactGap: 6, phoneAddrGap: -2, addrGap: -2,
  },
  'AMCR':       { nameEn: 'AMCR',      addressKr: '', addressEn: '', logo: LOGO_CLIP, logoWidth: 140, mapsUrl: '' },
  '스테이폴리오':  { nameEn: 'Stayfolio', addressKr: '', addressEn: '', logo: LOGO_CLIP, logoWidth: 140, mapsUrl: '' },
};

const DEFAULT_COMPANY = {
  nameEn: 'grandeclip',
  addressKr: '서울특별시 강남구 가로수길 14-4 (우.06036)',
  addressEn: 'grandeclip, 14-4, Garosu-gil, Gangnam-gu, Seoul, Korea, 06036',
  logo: LOGO_CLIP, logoWidth: 140, mapsUrl: MAPS_URL_GAROSU,
};

// 싱가포르 오피스(임원진 공통). 김봉진·설보미(GCK Co-Founder).
const SINGAPORE_OFFICE = {
  addressKr: '',
  addressEn: 'grandeclip, #36-02 One Raffles Place Tower 1,\n1 Raffles Place, Singapore, 048616',
  mapsUrl: 'https://maps.app.goo.gl/YZhbVagA7ZTQVsHS9',
};

// 구성원 개별 예외(소속 기본값 위에 덮어씀). 키 = '이름' 또는 '이름:소속'(소속별 다른 표기).
// 조회 순서: '이름:소속' 우선, '이름' 일반. 둘 다 있으면 머지(소속별이 우선).
const MEMBER_OVERRIDES = {
  '김봉진': {
    rankEn: 'Chairman', rankKr: '', teamKr: '', teamEn: '',   // "Chairman, grandeclip" (팀 없음)
    phone: '',   // 의장 — 연락처 미표기 (빈 값이면 base의 '+82 10 0000 0000' 템플릿이 뜨므로 명시적으로 ''로 숨김)
    ...SINGAPORE_OFFICE,
  },
  '설보미': {
    rankEn: 'General CEO',   // FnB 대표 (시트 영문직급은 Director지만 서명엔 General CEO)
    phone: '',                // 대표 — 연락처 미표기
    phoneAddrGap: 16,         // 전화 숨김이라 직책↔주소 사이 한 줄 띄움
  },
  '설보미:GCK': {
    rankEn: 'Co-Founder', rankKr: '', teamKr: '', teamEn: '',   // GCK 표기 → "Co-Founder｜grandeclip"
    phone: '',
    ...SINGAPORE_OFFICE,
    addrGap: 16,    // 국문주소 비어있어 영문주소 marginTop으로 직책↔주소 한 줄 띄움
  },
  '김소희': { titleOverride: 'People team Lead' },   // 피플팀 리드
  '김예슬': { titleOverride: 'glowbeast Lead' },     // glowbeast 리드
  '한승혜': { nameEn: 'Helena Han', titleOverride: 'Executive Assistant, Management Support' },  // 의장 비서(책임) — 역할 기준 표기. 시트 영문명(Seunghae Han)은 본인 선호명 Helena로 덮음
};

// 시트엔 없지만 웹앱에 추가로 매칭시킬 합성 구성원 (예: 다중 소속 임원).
const EXTRA_MEMBERS = [
  // 설보미님 — FnB 시트 데이터 외에 GCK Co-Founder로도 매칭되게.
  {
    name_kr: '설보미', name_kr_norm: '설보미', affiliation: 'GCK',
    name_en: 'Bomi Sul',
    team: '', team_en: '', title: '',
    rank_kr: '', rank_en: '', email: '', phone: '', birthdate: '',
  },
];

// 직급(한) × 직무 트랙 → 영문 직급. 사용자가 공유한 공식 매핑표 그대로.
// 트랙: General(기본) / Engineer / Editor / Designer
// (상무·전무의 Engineer·Editor는 직위 그대로 유지하는 "(동일)" 처리)
const RANK_BY_TRACK = {
  '사원':   { General: 'Associate',                  Engineer: 'AX Partner',                Editor: 'Editor',            Designer: 'Designer' },
  '주임':   { General: 'Senior Associate',           Engineer: 'AX Partner',                Editor: 'Editor',            Designer: 'Designer' },
  '선임':   { General: 'Assistant Manager',          Engineer: 'Senior AX Partner',         Editor: 'Editor',            Designer: 'Senior Designer' },
  '책임':   { General: 'Manager',                    Engineer: 'Lead AX Partner',           Editor: 'Editor',            Designer: 'Lead Designer' },
  '수석':   { General: 'Senior Manager',             Engineer: 'Principal AX Partner',      Editor: 'Editor',            Designer: 'Design Manager' },
  '이사':   { General: 'Director',                   Engineer: 'Director of AX Partner',    Editor: 'Editor in chief',   Designer: 'Design Director' },
  '상무':   { General: 'Executive Director',         Engineer: 'Director of AX Partner',    Editor: 'Editor in chief',   Designer: 'Head of Design' },
  '전무':   { General: 'Senior Executive Director',  Engineer: 'Director of AX Partner',    Editor: 'Editor in chief',   Designer: 'Chief Design Officer' },
  '부사장': { General: 'Vice President',             Engineer: '',                          Editor: '',                  Designer: '' },
  '대표':   { General: 'CEO',                        Engineer: 'CEO',                       Editor: 'CEO',               Designer: 'CEO' },
};

// 직접입력 모달 셀렉터용: 직무 트랙 칩(기본 일반) + 직급 드롭다운.
const TRACK_OPTIONS = [
  { value: 'General',  label: '일반' },
  { value: 'Designer', label: '디자인' },
  { value: 'Engineer', label: '개발' },
  { value: 'Editor',   label: '에디터' },
];
const RANK_OPTIONS = Object.keys(RANK_BY_TRACK); // 사원~대표

// 직무 텍스트 → 트랙. 직무가 마스터시트의 "Internal Branding", "Editor", "Designer" 같은 영문
// 또는 "엔지니어", "에디터" 같은 한글일 수 있어 키워드 휴리스틱으로 분류.
function detectTrack(title) {
  const t = (title || '').toLowerCase();
  if (/design|디자인|디자이너/.test(t)) return 'Designer';
  if (/engineer|developer|engineering|개발/.test(t)) return 'Engineer';
  if (/editor|에디터|기자|편집/.test(t)) return 'Editor';
  return 'General';
}

function lookupRankEn(rankKr, track) {
  const row = RANK_BY_TRACK[rankKr];
  if (!row) return '';
  return row[track] || row.General || '';
}

// 팀(한) → 영문. 마스터시트 영문팀명(메일용) 컬럼이 우선, 비어있을 때만 이 테이블 사용.
// 마스터시트에서 이미 확정된 영문 표기만 포함 — 추측은 빈칸으로 두고 사용자가 edit form에서 직접 입력.
const TEAM_KR_TO_EN = {
  '피플팀':         'People',
  '매거진사업부':    'Magazine C',
  '인사팀':         'HR',
  '경영관리팀':      'Finance & Accounting',
  '경영지원부문':    'Management Support',
  'GVC':           'Group Value up Center',
  'GVC 마케팅':     'Group Value up Center',
  'GVC 디자인':     'Group Value up Center',
  'GVC 디자인팀':   'Group Value up Center',
  '봉앤설이니셔티브': 'Bong&Sul Initiative',
};

// 소속 팀 드롭다운 옵션(편집 모달). 알려진 팀 선택 시 영문 팀명 자동 채움. 가나다 순 정렬.
// GVC는 'GVC' 하나로 통합(마케팅·디자인·디자인팀 세부는 드롭다운에서 제외). 봉앤설·경영지원부문도 제외.
const TEAM_OPTIONS = Object.keys(TEAM_KR_TO_EN)
  .filter(t => !['봉앤설이니셔티브', '경영지원부문', 'GVC 마케팅', 'GVC 디자인', 'GVC 디자인팀'].includes(t))
  .sort((a, b) => a.localeCompare(b, 'ko'));

function normalizeName(name) {
  return (name || '').replace(/\s+/g, '').toLowerCase();
}

// 한글 이름 → 영문명 자동 생성 (데이터 없는 사람의 영문명 prefill용. 사용자가 모달에서 수정 가능).
// 규칙: "성+이름"을 로마자화해 "<이름> <성>" 순서로. 항상 첫 글자만 대문자(Saebom Ryu).
//   FnB의 "성 전체 대문자(Saebom RYU)"는 데이터가 아니라 서명 렌더에서만 적용(surnameUpper). 시트엔 일반 표기.
const _ROMAN_INI = ['g','kk','n','d','tt','r','m','b','pp','s','ss','','j','jj','ch','k','t','p','h'];
const _ROMAN_MED = ['a','ae','ya','yae','eo','e','yeo','ye','o','wa','wae','oe','yo','u','wo','we','wi','yu','eu','ui','i'];
const _ROMAN_FIN = ['','k','k','k','n','n','n','t','l','k','m','l','l','l','p','l','m','p','p','t','t','ng','t','t','k','t','p','h'];
// 통용 성씨 표기 (로마자 규칙과 다른 관용 표기). 없으면 규칙 로마자화로 폴백.
const SURNAME_ROMAN = {
  '김':'Kim','이':'Lee','박':'Park','최':'Choi','정':'Jung','강':'Kang','조':'Cho','윤':'Yoon',
  '장':'Jang','임':'Lim','한':'Han','오':'Oh','서':'Seo','신':'Shin','권':'Kwon','황':'Hwang',
  '안':'Ahn','송':'Song','전':'Jeon','홍':'Hong','유':'Yoo','류':'Ryu','고':'Ko','문':'Moon',
  '양':'Yang','손':'Son','배':'Bae','백':'Baek','허':'Heo','남':'Nam','심':'Shim','노':'Noh',
  '하':'Ha','곽':'Kwak','성':'Sung','차':'Cha','주':'Joo','우':'Woo','구':'Koo','민':'Min',
  '나':'Na','지':'Ji','채':'Chae','천':'Cheon','방':'Bang','현':'Hyun','함':'Ham','변':'Byun',
  '염':'Yeom','추':'Chu','도':'Do','소':'So','석':'Seok','설':'Sul','마':'Ma','연':'Yeon',
  '경':'Kyung','봉':'Bong','엄':'Eom','원':'Won','인':'In','왕':'Wang','명':'Myung','기':'Ki',
};
const TWO_CHAR_SURNAMES = ['남궁','선우','황보','제갈','사공','서문','독고','동방'];

function romanizeSyllable(ch) {
  const code = ch.charCodeAt(0) - 0xAC00;
  if (code < 0 || code > 11171) return ch; // 한글 음절 아님 → 그대로
  return _ROMAN_INI[Math.floor(code / 588)] + _ROMAN_MED[Math.floor((code % 588) / 28)] + _ROMAN_FIN[code % 28];
}
function romanizeWord(kr) {
  return Array.from(kr).map(romanizeSyllable).join('');
}
function capFirst(s) { return s ? s[0].toUpperCase() + s.slice(1).toLowerCase() : s; }

function romanizeName(nameKr, affiliation) {
  const kr = (nameKr || '').replace(/\s+/g, '');
  if (!kr) return '';
  const twoChar = TWO_CHAR_SURNAMES.find(s => kr.startsWith(s));
  const surnameKr = twoChar || kr.slice(0, 1);
  const givenKr = kr.slice(surnameKr.length);
  const given = capFirst(romanizeWord(givenKr));
  const surname = capFirst(SURNAME_ROMAN[surnameKr] || romanizeWord(surnameKr));  // 항상 첫 글자만. FnB 대문자는 렌더에서.
  return given ? `${given} ${surname}` : surname;
}

// 전화는 단일 풀텍스트 입력칸. 데이터 있으면 "+82 10 XXXX XXXX"가 그대로 채워져 보이고 자유 편집.
function subDigits(raw) {  // 다양한 형태(010-…, +82 10 …)에서 가입자 8자리만 추출 (초기값 세팅용)
  let d = String(raw || '').replace(/\D/g, '');
  if (d.startsWith('82')) d = d.slice(2);   // 국가번호
  if (d.startsWith('0'))  d = d.slice(1);   // 010 → 10
  if (d.startsWith('10')) d = d.slice(2);   // 통신사 10
  return d.slice(0, 8);
}
function formatSub(d) {  // 가입자 숫자 → "XXXX XXXX"
  d = String(d || '').replace(/\D/g, '').slice(0, 8);
  return d.length <= 4 ? d : `${d.slice(0, 4)} ${d.slice(4)}`;
}
// 입력 정리: "+82 10"이 그대로면 뒤 가입자 8자리를 보기 좋게, 빈칸에 숫자만 치면 +82 10 자동 부착. 그 외(+1 등)는 그대로.
function smartPhone(raw) {
  const s = String(raw || '');
  const fmt = (d) => d.length <= 4 ? `+82 10 ${d}` : `+82 10 ${d.slice(0, 4)} ${d.slice(4)}`;
  const m = s.match(/^\+82 10\s*([\d ]*)$/);
  if (m) { const d = m[1].replace(/\D/g, '').slice(0, 8); return d ? fmt(d) : s; }
  if (/^[\d ]+$/.test(s)) return fmt(s.replace(/\D/g, '').slice(0, 8));
  return s;
}

// 이메일 입력은 아이디만 치고 "@<도메인>"은 고정 접미. 저장값은 풀 이메일.
function emailLocalPart(email) { const e = String(email || ''); return e.includes('@') ? e.slice(0, e.indexOf('@')) : e; }
function composeEmail(local, domain) { const id = String(local || '').replace(/[@\s]/g, ''); return id && domain ? `${id}@${domain}` : id; }

function getOrigin() {
  if (typeof window !== 'undefined' && window.location && window.location.origin) {
    return window.location.origin;
  }
  return '';
}

// 직급·팀의 영문 추정값. 사용자가 모달에서 직접 수정 가능.
function deriveRankEn(member) {
  const track = detectTrack(member.title);
  return member.rank_en || lookupRankEn(member.rank_kr || '사원', track) || '';  // 직급 데이터 없으면 '사원' 기준
}

function deriveTeamEn(member) {
  // GVC 계열은 시트 team_en('GVC')을 무시하고 'Group Value up Center'로 통일.
  if (String(member.team || '').startsWith('GVC')) return TEAM_KR_TO_EN['GVC'];
  // 글로우비스트팀은 시트 team_en('Glowbeast')을 무시하고 소문자 'glowbeast'로 통일.
  if (String(member.team || '').startsWith('글로우비스트')) return 'glowbeast';
  return member.team_en || TEAM_KR_TO_EN[member.team] || '';
}

// 미리보기 직급·소속 한 줄 조합: "<직급> of <팀>, <회사>"
// "of"와 ", " 구분자는 자동. 한 쪽이 비면 자연스럽게 생략.
function composeTitleLine(rankEn, teamEn, companyNameEn) {
  if (rankEn && teamEn) return `${rankEn} of ${teamEn}, ${companyNameEn}`;
  if (rankEn)           return `${rankEn}, ${companyNameEn}`;
  if (teamEn)           return `${teamEn}, ${companyNameEn}`;
  return companyNameEn;
}

// 직급·직책 표기의 '자동 조합값' (override 비었을 때 기본값). 회사명은 별도로 결합.
//  - GCK 등 팀 있는 경우: "직급, 팀" (C레벨/Chief는 팀 생략)
//  - noTeam(봉앤설·FnB): "직급"
function autoTitleCore(edit) {
  const isCLevel = /^chief/i.test((edit.rankEn || '').trim());
  if (edit.teamEn && !isCLevel) return `${edit.rankEn}, ${edit.teamEn}`;
  return edit.rankEn || '';
}

// 영문 주소에서 회사명을 분리 ("grandeclip, 14-4, ..." → ["grandeclip", ", 14-4, ..."])
// 회사명만 밑줄 처리해서 시각적 강조 (스크린샷 디자인).
function splitAddressEn(addressEn, companyNameEn) {
  if (!addressEn || !companyNameEn) return { lead: '', rest: addressEn || '' };
  if (addressEn.startsWith(companyNameEn)) {
    return { lead: companyNameEn, rest: addressEn.slice(companyNameEn.length) };
  }
  return { lead: '', rest: addressEn };
}

// 전화번호 표시문자열 → tel: 링크용 숫자 ("+82 10 9472 8425" → "+821094728425").
function telHref(phone) {
  const cleaned = String(phone || '').replace(/[^\d+]/g, '');
  return cleaned ? 'tel:' + cleaned : '';
}

// 외자(이름이 한 글자) 국문명은 성과 이름 사이를 한 칸 띄운다. "이주" → "이 주", "남궁준" → "남궁 준".
// 데이터(nameKr)는 붙여 두고 서명 렌더에서만 적용. 이름이 두 글자 이상이면 원본 그대로.
function spaceSingleGivenName(nameKr) {
  const kr = String(nameKr || '').replace(/\s+/g, '');
  if (!kr) return '';
  const two = TWO_CHAR_SURNAMES.find(s => kr.startsWith(s));
  const surname = two || kr.slice(0, 1);
  const given = kr.slice(surname.length);
  return given.length === 1 ? `${surname} ${given}` : kr;
}

// FnB 서명 전용: 영문명의 성(마지막 단어)만 대문자로. 데이터(시트)는 일반 표기로 두고 렌더에서만 변환.
// "Saebom Ryu" → "Saebom RYU". 단어가 하나뿐이면(성만/별명) 그대로.
function upperSurname(en) {
  const parts = String(en || '').trim().split(/\s+/);
  if (parts.length < 2) return en || '';
  parts[parts.length - 1] = parts[parts.length - 1].toUpperCase();
  return parts.join(' ');
}

// 메일 서명 미리보기 = 그대로 복사되는 서명 본체.
// 인라인 스타일만 사용 — 이 DOM의 outerHTML이 클립보드로 들어가 Gmail 서명이 되므로, 외부 CSS·클래스에
// 의존하면 안 된다. 폰트는 Pretendard 우선 + 시스템 폰트 폴백(메일 클라이언트는 웹폰트 미로딩).
const FONT_STACK = '"Pretendard Variable", "Pretendard", -apple-system, BlinkMacSystemFont, "Segoe UI", "Malgun Gothic", sans-serif';
const LINE_GAP   = 3; // 전화·주소KR·주소EN 줄 간격 (직급 줄은 위에서 따로 2px)

const SignaturePreview = React.forwardRef(({ edit }, ref) => {
  const { lead: addrLead, rest: addrRest } = splitAddressEn(edit.addressEn, edit.companyNameEn);
  // FnB는 성(영문)만 대문자로 렌더(surnameUpper). 데이터는 일반 표기.
  const nameEnDisplay = edit.surnameUpper ? upperSurname(edit.nameEn) : edit.nameEn;
  const nameKrDisplay = spaceSingleGivenName(edit.nameKr);   // 외자면 "이 주"처럼 성·이름 사이 한 칸
  const nameLine = nameEnDisplay ? `${nameKrDisplay} ${nameEnDisplay}` : nameKrDisplay;
  const phoneText = ((edit.phone || '').replace(/\D/g, '').length > 4) ? edit.phone : '';   // 숫자 있을 때만(빈/접두만이면 생략)
  // 봉앤설은 국문 직책 단독(회사명 안 붙임), 그 외는 "직급 of 팀, 회사" 영문 조합.
  // 직급·직책 표기: 입력칸 값(titleOverride)을 그대로 사용. (등록된 사람은 init에서 자동값으로 미리 채워둠)
  // 회사명 결합: GCK(barFormat)는 "코어｜회사", 그 외(봉앤설·FnB)는 "코어, 회사". 비면 회사명만.
  // 구분자는 전각 U+FF5C(｜). 박스드로잉 │(U+2502)보다 막대가 짧고 좌우 여백이 붙어 있어 따로 띄어쓰기 불필요.
  const titleCore = (edit.titleOverride || '').trim();
  const titleLine = !titleCore ? (edit.companyNameEn || '')
    : edit.barFormat ? (edit.companyNameEn ? `${titleCore}｜${edit.companyNameEn}` : titleCore)
    : (edit.companyNameEn ? `${titleCore}, ${edit.companyNameEn}` : titleCore);
  // 소속별 줄 들여쓰기 (FnB 가이드: 이름·직책 앞 2칸, 전화·주소 앞 3칸).
  // nbsp는 폰트 크기에 비례해 폭이 달라져(이름 22px vs 직책 13px → 같은 2칸도 다르게 보임) 줄이
  // 안 맞으므로, 폰트 무관하게 일정한 px padding으로 처리. 1칸 ≈ 6px(이름 2칸 ≈ 12px). 그 외 소속은 0.
  const INDENT_PX = 6;
  const namePadPx = (edit.nameIndent || 0) * INDENT_PX;
  const contactPadPx = (edit.contactIndent || 0) * INDENT_PX;

  return (
    <div
      ref={ref}
      style={{
        background: '#ffffff',
        /* padding 0 — 복사본이 좌측에 딱 붙어야 함. 미리보기 내부 여백은 .preview-box가 담당. */
        fontFamily: FONT_STACK,
        color: '#0A0A0A',
        lineHeight: 1.5,
        textAlign: 'left',
        boxSizing: 'border-box',
      }}
    >
      {edit.logo && edit.logoPosition !== 'bottom' && (
        edit.logoLink
          ? <a href={edit.logoLink} target="_blank" rel="noopener noreferrer" style={{ display: 'block', marginBottom: edit.logoGap }}>
              <img src={edit.logo} alt={edit.companyNameEn} width={edit.logoWidth} style={{ display: 'block', border: 0 }} />
            </a>
          : <img src={edit.logo} alt={edit.companyNameEn} width={edit.logoWidth} style={{ display: 'block', marginBottom: edit.logoGap, border: 0 }} />
      )}
      <div style={{ fontSize: edit.nameSize, fontWeight: 700, letterSpacing: '-0.015em', lineHeight: 1.15, color: '#0A0A0A', paddingLeft: namePadPx }}>{nameLine}</div>
      {titleLine && (
        <div style={{ marginTop: edit.nameTitleGap, fontSize: edit.titleSize, fontWeight: edit.titleStrong ? 700 : 400, color: edit.titleStrong ? '#1A1A1A' : '#2A2A2A', paddingLeft: namePadPx }}>{titleLine}</div>
      )}
      {edit.showEmail && edit.email && (
        <div style={{ marginTop: edit.contactGap, fontSize: 13, color: '#999999', paddingLeft: contactPadPx }}>
          <a href={'mailto:' + edit.email} style={{ color: '#1C46F3', textDecoration: 'none' }}>{edit.email}</a>
        </div>
      )}
      {phoneText && (
        <div style={{ marginTop: edit.contactGap, fontSize: edit.phoneSize, fontWeight: edit.phoneBold ? 700 : 400, color: '#999999', paddingLeft: contactPadPx }}>
          <a href={telHref(phoneText)} style={{ color: '#999999', textDecoration: 'none', fontWeight: edit.phoneBold ? 700 : 400 }}>{phoneText}</a>
        </div>
      )}
      {edit.addressKr && (
        <div style={{ marginTop: edit.phoneAddrGap, fontSize: edit.addrSize, color: '#999999', paddingLeft: contactPadPx }}>{edit.addressKr}</div>
      )}
      {edit.addressEn && (
        // 한글주소↔영문주소 간격(edit.addrGap, 기본 0=line-height). FnB는 음수로 살짝 좁힘.
        <div style={{ marginTop: edit.addrGap, fontSize: edit.addrSize, color: '#999999', paddingLeft: contactPadPx }}>
          {addrLead && (
            // textDecorationSkipInk: 'none' — 밑줄이 'g'·'p' 같은 디센더를 피해가지 않고 글자 전체에 연결되게.
            // (기본값 auto면 "grandeclip"의 'g' 밑이 끊겨 보임)
            edit.mapsUrl
              ? <a href={edit.mapsUrl} target="_blank" rel="noopener noreferrer" style={{ color: '#999999', textDecoration: 'underline', textDecorationSkipInk: 'none' }}>{addrLead}</a>
              : <span style={{ textDecoration: 'underline', textDecorationSkipInk: 'none' }}>{addrLead}</span>
          )}
          {addrRest.split('\n').map((line, i) => (
            <React.Fragment key={i}>{i > 0 && <br />}{line}</React.Fragment>
          ))}
        </div>
      )}
      {edit.logo && edit.logoPosition === 'bottom' && (
        // 봉앤설: 컬러 배너 로고를 주소 아래(맨 밑)에. logoLink 있으면 클릭 시 해당 URL 오픈.
        edit.logoLink
          ? <a href={edit.logoLink} target="_blank" rel="noopener noreferrer" style={{ display: 'block', marginTop: 14 }}>
              <img src={edit.logo} alt={edit.companyNameEn} width={edit.logoWidth} style={{ display: 'block', border: 0 }} />
            </a>
          : <img src={edit.logo} alt={edit.companyNameEn} width={edit.logoWidth} style={{ display: 'block', marginTop: 14, border: 0 }} />
      )}
    </div>
  );
});

// 미리보기 DOM(인라인 스타일)의 HTML을 그대로 클립보드에 — Gmail 서명란에 붙여넣으면 실제 텍스트
// 서명이 된다. text/html(서식 유지) + text/plain(폴백) 둘 다 입력.
async function copySignatureHtml(el) {
  if (document.fonts && document.fonts.ready) {
    try { await document.fonts.ready; } catch (_) {}
  }
  const html = el.outerHTML;
  const text = el.innerText || '';

  if (window.ClipboardItem && navigator.clipboard?.write) {
    try {
      await navigator.clipboard.write([new ClipboardItem({
        'text/html':  new Blob([html], { type: 'text/html' }),
        'text/plain': new Blob([text], { type: 'text/plain' }),
      })]);
      return true;
    } catch (e) {
      console.warn('ClipboardItem 실패, execCommand로 폴백', e);
    }
  }

  // 폴백: 렌더된 서명을 선택 → execCommand('copy')로 서식 포함 복사 (구버전 브라우저).
  try {
    const range = document.createRange();
    range.selectNode(el);
    const sel = window.getSelection();
    sel.removeAllRanges();
    sel.addRange(range);
    const ok = document.execCommand('copy');
    sel.removeAllRanges();
    return ok;
  } catch (e) {
    console.warn('execCommand 폴백 실패', e);
    return false;
  }
}

// 로그인 화면: 소속 + 이름 + 생년월일 → Apps Script 인증. 성공 시 onSuccess(member).
// PII는 인증 통과 후 "본인 1건"만 받아온다. (이전의 전체 members.json 공개 방식 폐기)
const LoginScreen = ({ onSuccess }) => {
  const [name, setName] = React.useState('');
  const [affiliation, setAffiliation] = React.useState('');
  const [birthdate, setBirthdate] = React.useState('');
  const [error, setError] = React.useState('');
  const [busy, setBusy] = React.useState(false);
  const today = new Date().toISOString().slice(0, 10);

  const configured = !!AUTH_API_URL;

  const submit = async () => {
    setError('');
    if (!configured) { setError('인증 서버가 아직 설정되지 않았어요. 피플팀에 문의해주세요.'); return; }
    if (!name.trim())   { setError('이름을 입력해주세요.'); return; }
    if (!affiliation)   { setError('소속을 선택해주세요.'); return; }
    if (!birthdate)     { setError('생년월일을 입력해주세요.'); return; }
    setBusy(true);
    try {
      // text/plain → 프리플라이트(OPTIONS) 없이 전송(Apps Script CORS 우회). 본문은 JSON 문자열.
      const res = await fetch(AUTH_API_URL, {
        method: 'POST',
        headers: { 'Content-Type': 'text/plain;charset=utf-8' },
        body: JSON.stringify({ name: name.trim(), affiliation, birthdate }),
      });
      const data = await res.json();
      if (data && data.ok && data.member) {
        onSuccess(data.member);
      } else {
        // 일치하는 구성원이 없음 → 안내·문의 메시지 없이 곧바로 직접 입력 화면으로. (본인이 입력하는 정보라 PII 노출 없음)
        startManual();
      }
    } catch (e) {
      // 인증 서버 장애 시에도 멈추지 않고 직접 입력으로 진행.
      startManual();
    } finally {
      setBusy(false);
    }
  };

  // 직접 입력: 입력한 이름 + 소속으로 빈 서명 골격을 만들어 편집 화면으로. (생년월일·인증 불필요)
  const startManual = () => {
    setError('');
    if (!name.trim()) { setError('이름을 입력해주세요.'); return; }
    if (!affiliation) { setError('소속을 선택해주세요.'); return; }
    onSuccess({
      name_kr: name.trim(),
      name_en: romanizeName(name.trim(), affiliation),  // 영문명 추정 prefill (편집창에서 수정)
      affiliation,
      team: '', team_en: '', title: '',
      rank_kr: '', rank_en: '', email: '', phone: '', birthdate: '',
      __manual: true,   // App이 편집창을 바로 열도록 표시
    });
  };

  return (
    <div className="page">
      <div className="card">
        <div className="login-head">
          <div className="login-title">메일 서명 만들기</div>
          <div className="login-sub">🔒 구성원 개인정보 보호를 위해 간단한 본인 확인 절차를 진행합니다.</div>
        </div>
        <div className="form-row">
          <label className="form-label" htmlFor="login-name">이름</label>
          <input
            id="login-name"
            className="form-input"
            value={name}
            onChange={e => setName(e.target.value)}
            onKeyDown={e => { if (e.key === 'Enter') submit(); }}
            autoComplete="off"
          />
        </div>
        <div className="form-row">
          <label className="form-label" htmlFor="login-birth">생년월일</label>
          <input
            id="login-birth"
            className="form-input"
            type="date"
            max={today}
            value={birthdate}
            onChange={e => setBirthdate(e.target.value)}
            onKeyDown={e => { if (e.key === 'Enter') submit(); }}
          />
        </div>
        <div className="form-row">
          <label className="form-label">소속</label>
          <div className="radio-group" role="radiogroup" aria-label="소속">
            {AFFILIATION_OPTIONS.map(opt => (
              <label key={opt.value} className={`radio-chip${affiliation === opt.value ? ' is-selected' : ''}`}>
                <input
                  type="radio"
                  name="login-affiliation"
                  value={opt.value}
                  checked={affiliation === opt.value}
                  onChange={() => setAffiliation(opt.value)}
                />
                <span className="radio-label">{opt.label}</span>
              </label>
            ))}
          </div>
        </div>
        <button className="btn btn-primary" onClick={submit} disabled={busy}>
          {busy ? '확인 중...' : '확인'}
        </button>
        {error && <div className="error">{error}</div>}
      </div>
    </div>
  );
};

const App = () => {
  const [authed, setAuthed] = React.useState(null);   // 인증된 본인 레코드(null이면 로그인 화면)
  const [match, setMatch] = React.useState(null);
  // edit = 미리보기에 표시되는 6개 필드. 매칭 결과(match)를 derive해서 초기화하고,
  // 이후 사용자가 상단 입력 폼에서 자유롭게 수정. 미리보기는 이 state로부터 렌더.
  const [edit, setEdit] = React.useState(null);
  const [error, setError] = React.useState('');
  const [toast, setToast] = React.useState('');
  const [copying, setCopying] = React.useState(false);
  const [showEdit, setShowEdit] = React.useState(false);
  const sigRef = React.useRef(null);
  const editBackupRef = React.useRef(null);  // 수정창 열 때 상태 백업 → 취소(X/바깥클릭) 시 복원, 완료 시 갱신

  // match가 잡히면 edit 초기화. match가 null이면 edit도 비움.
  React.useEffect(() => {
    if (!match) { setEdit(null); return; }
    const company = COMPANY_BY_AFFILIATION[match.affiliation] || DEFAULT_COMPANY;
    // 개인 예외: '이름:소속'(소속별) + '이름'(일반) 머지. 소속별이 우선.
    const _nn = normalizeName(match.name_kr);
    const generic  = MEMBER_OVERRIDES[_nn] || MEMBER_OVERRIDES[match.name_kr] || {};
    const specific = MEMBER_OVERRIDES[`${_nn}:${match.affiliation}`] || MEMBER_OVERRIDES[`${match.name_kr}:${match.affiliation}`] || {};
    const override = { ...generic, ...specific };
    // 소속 팀: GVC 세부팀은 'GVC'로 통합. 팀 데이터 없으면 company.defaultTeam(GCK=GVC)으로. 영문은 매핑/시트값.
    const teamKr = company.noTeam ? ''
      : (String(match.team || '').startsWith('GVC') ? 'GVC' : (match.team || company.defaultTeam || ''));
    const teamEn = company.noTeam ? '' : (deriveTeamEn(match) || TEAM_KR_TO_EN[teamKr] || '');
    const sub = subDigits(match.phone);   // 가입자 8자리(없으면 빈 값 → 회색 placeholder)
    const rankEn = deriveRankEn(match);
    const base = {
      affiliation:   match.affiliation,             // 트랙 옵션 필터(FnB 에디터 제외)용
      nameKr:        match.name_kr || '',
      nameEn:        match.name_en || '',
      rankEn:        rankEn,
      rankKr:        match.rank_kr || '사원',        // 직급 드롭다운용. 데이터 없으면 자동 '사원'
      track:         detectTrack(match.title),      // 직무 트랙용 (기본 General)
      teamKr:        teamKr,
      teamEn:        teamEn,
      titleKr:       company.koreanTitle ? (match.rank_kr || '') : '',  // (koreanTitle 옵션용. 현재 봉앤설은 B방식이라 미사용)
      koreanTitle:   !!company.koreanTitle,
      barFormat:     !!company.barFormat,             // GCK: "직급, 팀｜회사" 양식
      titleNoCompany:!!company.titleNoCompany,     // 봉앤설: 직급 뒤에 회사명 안 붙임
      email:         match.email || (company.showEmail && company.emailDomain ? `name@${company.emailDomain}` : ''),  // 봉앤설: 데이터 없으면 name@bongnsul.com 기본 채움(수정 가능)
      showEmail:     !!company.showEmail,
      emailDomain:   company.emailDomain || '',    // 이메일 입력 시 아이디 뒤에 자동으로 붙는 도메인
      phone:         sub ? `+82 10 ${formatSub(sub)}` : '+82 10 0000 0000',   // 데이터 있으면 채움 / 비면 0000 0000 템플릿 (개인 예외(김봉진/설보미)는 override가 ''로 숨김)
      addressKr:     company.addressKr || '',
      addressEn:     company.addressEn || '',
      companyNameEn: company.nameEn,  // titleLine 조합 + 영문 주소 underline 매칭에 사용
      logo:          company.logo || LOGO_CLIP,
      logoWidth:     company.logoWidth || DEFAULT_LOGO_WIDTH,
      logoPosition:  company.logoPosition || 'top',   // 봉앤설은 'bottom'(컬러 배너를 주소 아래에)
      logoGap:       company.logoGap ?? 10,            // 로고↔이름 간격(px). FnB는 좁게
      logoLink:      company.logoLink || '',           // 로고 클릭 시 열리는 링크 (봉앤설=bongnsul.com)
      mapsUrl:       company.mapsUrl || '',
      noTeam:        !!company.noTeam,
      nameSize:      company.nameSize || 22,    // 이름 글자 크기(px). FnB=17, 기본=22
      titleSize:     company.titleSize || 13,   // 직책 글자 크기(px). FnB=14, 기본=13
      nameTitleGap:  company.nameTitleGap ?? 2, // 이름↔직책 간격(px). FnB는 약간 좁게
      phoneSize:     company.phoneSize || 13,   // 전화 글자 크기(px). FnB는 약간 작게
      addrSize:      company.addrSize || 12,    // 주소(국·영) 글자 크기(px). FnB는 약간 작게
      titleStrong:   !!company.titleStrong,     // FnB: 직책 볼드+진한색
      surnameUpper:  !!company.surnameUpper,     // FnB: 서명에서 성(영문)만 대문자 렌더
      phoneBold:     !!company.phoneBold,   // FnB: 전화 볼드+진한색
      nameIndent:    company.nameIndent || 0,
      contactIndent: company.contactIndent || 0,
      contactGap:    company.contactGap ?? LINE_GAP,   // 전화·주소·영문주소 줄간격(px). GCK=1, 봉앤설=0(주소 간격과 동일), 그 외 기본 3
      phoneAddrGap:  company.phoneAddrGap ?? company.contactGap ?? LINE_GAP,  // 전화↔주소 간격. 봉앤설=16(한 줄 띄움), FnB=음수(좁게)
      addrGap:       company.addrGap ?? 0,             // 국문주소↔영문주소 간격. 기본 0(line-height), FnB는 음수로 살짝 좁힘
    };
    const finalEdit = { ...base, ...override };   // 구성원 개별 예외(김봉진 등)를 마지막에 덮어씀
    // 직급·직책 표기 기본값: 예외 적용된 '최종' 직급·팀 기준으로 자동 조합 (override가 직접 지정했으면 그대로).
    if (finalEdit.titleOverride == null) finalEdit.titleOverride = match.position || autoTitleCore(finalEdit);
    setEdit(finalEdit);
    editBackupRef.current = finalEdit;   // 매칭 직후의 자동 상태 = 복원 기준점
  }, [match]);

  // 수정창 취소(X/바깥 클릭): 열기 전 상태로 복원. 완료: 현재 상태를 새 복원 기준점으로 저장.
  const cancelEdit  = () => { if (editBackupRef.current) setEdit(editBackupRef.current); setShowEdit(false); };
  const confirmEdit = () => { editBackupRef.current = edit; setShowEdit(false); };

  const updateEdit = (field) => (e) => setEdit(prev => ({ ...prev, [field]: e.target.value }));

  // 저장된 세션 복원: 12시간 내 로그인한 본인 레코드면 재로그인 없이 복귀.
  React.useEffect(() => {
    try {
      const raw = localStorage.getItem(SESSION_KEY);
      if (!raw) return;
      const saved = JSON.parse(raw);
      if (!saved || !saved.member || !saved.exp || Date.now() > saved.exp) {
        localStorage.removeItem(SESSION_KEY);
        return;
      }
      setAuthed(saved.member);
      setMatch(saved.member);
    } catch (_) { try { localStorage.removeItem(SESSION_KEY); } catch (__) {} }
  }, []);

  // 인증 성공 → 본인 레코드를 세션에 저장하고 미리보기로 진입.
  const handleAuthed = (member) => {
    setAuthed(member);
    setMatch(member);
    setError('');
    // 직접 입력으로 들어온 경우: 골격만 채워졌으니 편집창을 바로 띄워 본인이 채우게 한다.
    if (member.__manual) setShowEdit(true);
    try {
      localStorage.setItem(SESSION_KEY, JSON.stringify({ member, exp: Date.now() + SESSION_TTL_MS }));
    } catch (_) { /* 저장 실패해도 동작은 계속 */ }
  };

  const onCopy = async () => {
    if (!match || !sigRef.current) return;
    setError('');
    setCopying(true);
    try {
      const ok = await copySignatureHtml(sigRef.current);
      if (ok) {
        setToast('서명이 복사됐어요! Gmail 서명란에 붙여넣기 (Ctrl+V)');
        setTimeout(() => setToast(''), 2800);
      } else {
        setError('복사 실패 — 브라우저 권한을 확인해주세요. (Chrome/Edge 권장)');
      }
    } catch (e) {
      console.error(e);
      setError('복사 실패: ' + (e?.message || e));
    } finally {
      setCopying(false);
    }
  };

  // 로그아웃: 세션·본인 레코드 제거 → 로그인 화면으로.
  const logout = () => {
    setMatch(null);
    setAuthed(null);
    setEdit(null);
    setShowEdit(false);
    setError('');
    try { localStorage.removeItem(SESSION_KEY); } catch (_) {}
  };

  // 미인증 → 로그인 화면. 인증 후에만 본인 데이터/미리보기 노출.
  if (!authed) {
    return <LoginScreen onSuccess={handleAuthed} />;
  }

  return (
    <div className="page">
      <div className="card card-wide">
        {match && edit && (
          <>
            <div className="two-col">
              <div className="col-left">
                <div className="preview-box">
                  <SignaturePreview ref={sigRef} edit={edit} />
                </div>
                {error && <div className="error">{error}</div>}
              </div>
              <div className="col-right">
                <div className="actions">
                  <button className="btn btn-ghost" onClick={() => setShowEdit(true)}>수정하기</button>
                  <button className="btn btn-primary" onClick={onCopy} disabled={copying}>
                    {copying ? '복사 중...' : '복사하기'}
                  </button>
                </div>
                <div className="help">
                  <b>Gmail에 등록하는 법</b>
                  <ol>
                    <li>
                      <a href="https://mail.google.com/mail/u/0/#settings/general" target="_blank" rel="noopener noreferrer">Gmail 설정<span className="ext-arrow" aria-hidden="true">↗</span></a> → 아래로 내려 <b>서명</b> 찾기 → <b>새로 만들기</b>
                    </li>
                    <li>오른쪽 편집창 클릭 → <b>Ctrl+V</b>로 붙여넣기</li>
                    <li><b>서명 기본값</b>을 모두 <b>방금 만든 서명</b>으로 선택</li>
                    <li>맨 아래 <b>변경사항 저장</b> 클릭</li>
                  </ol>
                </div>
                <div className="footer-action">
                  <a className="reset-link" href="#" onClick={e => { e.preventDefault(); logout(); }}>로그아웃</a>
                </div>
              </div>
            </div>
          </>
        )}

        {match && edit && showEdit && (
          <div className="modal-overlay" onClick={cancelEdit}>
            <div className="modal" onClick={e => e.stopPropagation()} role="dialog" aria-label="서명 정보 편집">
              <div className="modal-header">
                <button className="modal-close" onClick={cancelEdit} aria-label="닫기">×</button>
              </div>
              <div className="modal-body">
                <div className="form-row-pair">
                  <div className="form-row">
                    <label className="form-label">이름</label>
                    <input className="form-input" value={edit.nameKr} onChange={updateEdit('nameKr')} />
                  </div>
                  <div className="form-row">
                    <label className="form-label">영문명</label>
                    <input className="form-input" value={edit.nameEn} onChange={updateEdit('nameEn')} />
                  </div>
                </div>
                {/* 직무·직급·소속팀은 자동 데이터로 들어가므로 편집창에선 없앰.
                    대신 '직급·직책 표기' 한 칸으로 — 비우면 자동 조합, 직접 쓰면 그게 우선. */}
                <div className="form-row">
                  <label className="form-label">직급·직책 표기</label>
                  <input className="form-input" value={edit.titleOverride} onChange={updateEdit('titleOverride')} />
                </div>
                {edit.showEmail && (
                  <div className="form-row">
                    <label className="form-label">이메일</label>
                    <input className="form-input" type="email" value={edit.email} onChange={updateEdit('email')}
                      autoComplete="off" />
                  </div>
                )}
                <div className="form-row">
                  <label className="form-label">전화번호</label>
                  <input className="form-input" value={edit.phone}
                    onChange={e => setEdit(prev => ({ ...prev, phone: smartPhone(e.target.value) }))}
                    placeholder="+82 10 0000 0000" inputMode="tel" />
                </div>
                <div className="form-row">
                  <label className="form-label">주소</label>
                  {/* 국문(첫 줄, 한글일 때) + 영문(나머지). 국문 없는 경우(예: 싱가포르 오피스)는 영문만 표시 — 앞 빈줄 없음. */}
                  <textarea
                    className="form-input"
                    rows={3}
                    style={{ resize: 'vertical', lineHeight: 1.5 }}
                    value={[edit.addressKr, edit.addressEn].filter(Boolean).join('\n')}
                    onChange={e => {
                      const v = e.target.value;
                      const nl = v.indexOf('\n');
                      const firstLine = nl === -1 ? v : v.slice(0, nl);
                      const hasHangul = /[ㄱ-ㆎ가-힣]/.test(firstLine);
                      setEdit(prev => ({
                        ...prev,
                        addressKr: hasHangul ? firstLine : '',
                        addressEn: hasHangul ? (nl === -1 ? '' : v.slice(nl + 1)) : v,
                      }));
                    }}
                  />
                </div>
                <div className="form-row">
                  <label className="form-label">주소 하이퍼링크</label>
                  <input className="form-input" value={edit.mapsUrl} onChange={updateEdit('mapsUrl')} />
                </div>
              </div>
              <div className="modal-footer">
                <button className="btn btn-primary" onClick={confirmEdit}>완료</button>
              </div>
            </div>
          </div>
        )}
      </div>
      {toast && <div className="toast">{toast}</div>}
    </div>
  );
};

ReactDOM.createRoot(document.getElementById('root')).render(<App />);
