반응형

 

# 1) main 최신화
git switch main
git pull

# 2) main을 더럽히지 않게 시험용 브랜치에서 합친다
git switch -c try/pr7-9

# 3) PR 두 개를 로컬 브랜치로 가져와 머지
git fetch origin pull/7/head:pr-7 pull/9/head:pr-9
git merge --no-ff pr-7
git merge --no-ff pr-9

# 4) 확인
npm install        # package.json 이 바뀐 PR이면
npm run dev

되돌릴 때:

git merge --abort                        # 충돌 상태에서 빠져나올 때
git switch main
git branch -D try/pr7-9 pr-7 pr-9        # 시험 브랜치·PR 브랜치 정리
반응형
반응형
반응형
반응형

서버리스 (Vercel/Netlify)

  • 요청이 올 때만 함수가 잠깐 깨어나 실행 → 끝나면 사라짐
  • 장점: 안 쓸 때 비용 0, 자동 확장
  • 단점: 상시 연결(WebSocket)·긴 작업·백그라운드 크론 어려움

상시 서버 (Render/Railway)

  • 컨테이너가 24시간 계속 켜져 있음 (전통적인 서버 방식)
  • 장점: Express 그대로 실행, 무엇이든 가능
  • 단점: 안 써도 켜져 있으니 비용 발생(무료 티어는 슬립 걸림)

그래서 경계가 흐릿합니다

  • Vercel도 서버리스로 백엔드를 돌릴 수 있고,
  • Render/Railway도 정적 프론트를 올릴 수 있습니다.

서버리스가 아닌 상시 서버가 꼭 필요한 기능

상시 서버가 꼭 필요한 기능은 서버리스 환경의 제약사항(제한된 실행 시간, 콜드 스타트, 상태 저장 불가)으로 인해 발생합니다. 주로 15분 이상의 장기 실행 작업, 지속적인 클라이언트 연결 처리, 실시간 데이터 스트리밍에 필수적입니다. [1, 2]
대표적으로 상시 서버가 반드시 필요한 기능은 다음과 같습니다.
  • 실시간 양방향 통신 및 스트리밍: 소켓(WebSocket)이나 WebRTC를 활용한 실시간 채팅, 영상 스트리밍, 원격 제어, 멀티플레이어 게임 서버 등은 클라이언트와 지속적인 연결(Stateful)을 유지해야 하므로 상시 서버가 필수적입니다. [1]
  • 장기 실행 백그라운드 작업: 1회 실행 제한 시간(예: AWS Lambda 기준 최대 15분)을 초과하는 대용량 데이터 처리, 배치 작업(Batch Processing), 머신러닝 모델 학습 및 데이터 백업 등은 상시 구동되는 서버가 필요합니다.
  • 상태 정보(State) 유지 및 캐싱: 로그인 세션 정보나 실시간 게임 상태 등 지속적으로 메모리에 유지되어야 하는 상태 값을 다루는 기능입니다. 서버리스는 요청마다 상태가 초기화되기 때문에 연속적인 데이터 관리에 불리합니다. [1, 2]
  • 엄격한 실시간 응답 (지연 시간 최소화): 서버리스는 첫 요청 시 서버가 초기화되는 '콜드 스타트(Cold Start)'로 인해 응답 지연이 발생할 수 있습니다. 금융 거래, 자율주행 데이터 처리, 실시간 입찰 시스템처럼 0.001초의 지연도 치명적인 기능은 상시 서버를 사용해야 합니다. [1]
  • 특정 OS 및 하드웨어 제어: GPU가 반드시 필요한 작업이거나, 시스템 레벨의 커널 설정 및 특정 운영체제(OS)의 라이브러리를 직접 수정해야 하는 기능은 클라우드 제공업체의 통제를 받는 서버리스 환경에서 구현할 수 없습니다.
각자의 서비스 목적과 트래픽 규모에 맞는 아키텍처를 선택하기 위해 클라우드 서비스 제공업체에서 제공하는 AWS 컴퓨팅 서비스Google Cloud 컴퓨팅 엔진 의 옵션 및 특징을 비교해 볼 수 있습니다.
 

 


Vercel/Netlify 같은 정적 호스팅, 백엔드는 별도 서버(Render/Railway 등)???
네 곳 모두 코드를 올리면 인터넷에 띄워주는 호스팅입니다. 차이는 어떤 실행 방식을 주력으로 미느냐입니다.

서비스 주력 방식 잘 맞는 것
Vercel / Netlify 정적 파일 + 서버리스(요청 올 때만 실행) 프론트엔드, Next.js, 가벼운 API
Render / Railway 상시 켜진 서버(항상 돌아감) Express, DB, WebSocket, 크론, 무거운 작업
반응형
반응형

두 개의 독립적인 관문이 있고, anon SELECT 는 둘 다 통과해야 합니다. RLS 는 그 중 하나일 뿐이에요.

판단 순서

   anon key로 SELECT 요청
          ↓
  ① GRANT SELECT ON table TO anon    ← 테이블 레벨 권한 (SQL 표준)
     ↓ 없으면 → permission denied 에러
  ② RLS enabled?
     ├─ NO  → 통과, 전체 row 반환
     └─ YES → ③ 이 SELECT 를 허용하는 정책이 있나?
                 ├─ 없음 → 0건 반환 (에러 아님!)
                 └─ 있음 → 정책의 USING 조건 만족하는 row 만 반환

진리표

RLS anon 에 SELECT GRANT SELECT 정책 결과
OFF (무관) permission denied for table ... 에러
OFF (무관) 전체 row 반환
ON (무관) permission denied 에러
ON 없음 ⚠️ 빈 배열 [] (에러 없음, row 만 0건)
ON USING (true) 같은 관대한 정책 ✅ 전체 row 반환
ON USING (user_id = auth.uid()) 같은 조건 정책 정책 조건 통과한 row 만 반환

핵심 함정: RLS on + 정책 없음 = 0건 (에러 없음)

이게 지난번 getResidence 404 사건의 원인이었어요. 앱 입장에서 보면:

const { data, error } = await supabase.from('decozip_residence')...
// error = null   ← 에러 아님!
// data  = null   ← 그냥 row 가 없다고 나옴

앱은 "row 가 존재하지 않는다"로 해석 → notFound() → 404. DB 에는 row 가 실제로 있어도 그렇게 됩니다.

이래서 "RLS 는 fail-closed(기본 거부)" 라고 표현해요. SQL 의 기본 조회 결과와 자연스럽게 통합돼서 티가 안 남.

Supabase 특수 사항

Supabase 는 프로젝트 생성 시 몇 가지 자동 설정을 해두어서 헷갈릴 수 있어요:

  1. 자동 GRANT: 새 테이블 생성 시 anon, authenticated, service_role 에 CRUD 를 자동으로 GRANT. 그래서 ① 관문은 자동 통과하는 경우가 대부분.
  2. 자동 RLS ON: Dashboard 로 테이블 생성하면 RLS 가 켜진 상태로 시작하는 게 요즘 기본값 (2023 이후). SQL 로 CREATE TABLE 하면 꺼진 상태.
  3. 경고 배너: RLS 를 껐거나 정책 없이 켜두면 Dashboard 에 "Insecure" 배지가 표시됨.

지난번 문제도 이 조합에서 발생: ALTER TABLE ... ADD COLUMN 하거나 Dashboard 에서 컬럼 편집하면 종종 RLS 스위치가 자동으로 켜져요. 그 순간부터 정책 없으니 anon 조회 결과가 다 0건이 되고, 앱은 404.

결정적 차이 요약

  • GRANT = "이 role 이 이 테이블을 건드릴 수 있는가?" — 테이블 단위
  • RLS = "이 role 이 이 특정 row 를 볼/쓸 수 있는가?" — row 단위

GRANT 는 SQL 표준의 20년 넘은 개념, RLS 는 PostgreSQL 이 나중에 얹은 세밀한 필터. 둘은 각기 다른 층에서 동작하며 AND 조건으로 결합됩니다.

실전 검증 명령

-- ① 관문 (GRANT 상태)
SELECT has_table_privilege('anon', 'public.decozip_residence', 'SELECT') AS anon_can_select;

-- ② + ③ 관문 (RLS 상태 + 정책 개수)
SELECT
  c.relrowsecurity AS rls_on,
  (SELECT COUNT(*) FROM pg_policies p
    WHERE p.schemaname='public' AND p.tablename='decozip_residence'
      AND p.cmd IN ('SELECT','ALL')
      AND ('anon' = ANY(p.roles) OR 'public' = ANY(p.roles))
  ) AS anon_select_policies
FROM pg_class c
WHERE c.oid = 'public.decozip_residence'::regclass;
  • anon_can_select = true + rls_on = false전 row 조회 OK
  • anon_can_select = true + rls_on = true + anon_select_policies = 00건 (지난 404 상황)
  • anon_can_select = true + rls_on = true + anon_select_policies ≥ 1정책 조건대로
  • anon_can_select = false에러 (GRANT 부터 문제)
반응형
반응형
반응형
반응형

진단 SQL (Supabase SQL Editor)

-- 1) RLS 켜져 있는지 확인
SELECT
  schemaname, tablename,
  rowsecurity                                        AS rls_enabled,
  (SELECT COUNT(*) FROM pg_policies p
   WHERE p.schemaname='public' AND p.tablename='decozip_residence') AS policy_count
FROM pg_tables
WHERE schemaname='public' AND tablename='decozip_residence';

-- 2) 정책이 있으면 어떤 정책인지
SELECT policyname, cmd, roles, qual, with_check
FROM pg_policies
WHERE schemaname='public' AND tablename='decozip_residence';

rls_enabled = true 이고 policy_count = 0 이면 확정

해결방안

A. RLS 끄기 (가장 빠름, 현재 코드 그대로 동작)

ALTER TABLE public.decozip_residence DISABLE ROW LEVEL SECURITY;
운영 초기 단계고 anon 키가 유출된 상태가 아니면 이게 가장 간단. 다른 테이블들도 대부분 RLS 없이 동작 중이니 일관성도 유지됩니다.

B. RLS 유지 + anon SELECT 정책 추가

ALTER TABLE public.decozip_residence ENABLE ROW LEVEL SECURITY;

DROP POLICY IF EXISTS residence_select_all ON public.decozip_residence;
CREATE POLICY residence_select_all ON public.decozip_residence
FOR SELECT USING (true);

-- INSERT/UPDATE 도 필요하면 각각 별도 정책 (신규 등록·수정 화면용)
DROP POLICY IF EXISTS residence_insert_all ON public.decozip_residence;
CREATE POLICY residence_insert_all ON public.decozip_residence
FOR INSERT WITH CHECK (true);

DROP POLICY IF EXISTS residence_update_all ON public.decozip_residence;
CREATE POLICY residence_update_all ON public.decozip_residence
FOR UPDATE USING (true) WITH CHECK (true);
decozip_chat_message 이 이 패턴으로 되어 있어요 (data/public.decozip_chat_message.sql:42-50).

반응형
반응형

REPLACE(l.display_name, ' ', '')

TRANSLATE(l.display_name, ' /-·.', '')

특수문자 여러개 빼고 비교하고 싶다면 TRANSLATE를 사용하자!

반응형

'db' 카테고리의 다른 글

supabase-postgresql: rls, anon에 select grant  (0) 2026.07.02
postgresql - RLS 정책  (0) 2026.07.01
postgresql (IS NOT TRUE) === (!=TRUE or IS NULL )  (0) 2026.07.01
[postgresql] timestamp vs timestamptz  (0) 2026.06.25
DBeaver mariaDB 연결  (0) 2024.05.10
반응형

!= true쓰지 말고 is not true로 사용하자

반응형
반응형

CREATE OR REPLACE VIEW public.v_leads -> view는 삭제하고 재생성해야함, 삭제안하고 시도하면 에러남 ( ERROR: cannot change name of view column "_install_date" to "status_kr" , 기존이랑 컬럼수가 달라져서 그런것인지...?)
AS SELECT

CASE b.status::varchar -> status가 enum이라면 casting해줘야함, 안하면 에러남
WHEN 'consultation_pending' THEN '상담 전'
WHEN 'consultation_ended' THEN '상담종료'
WHEN 'no_response' THEN '반응 무'
WHEN 'qualified' THEN '가능고객'
WHEN 'hot_lead' THEN '가망고객'
WHEN 'quote_sent' THEN '견적 발송
WHEN 'contracted_no_date' THEN '시공일미정'
WHEN 'contracted' THEN '계약 완료'
WHEN 'completed' THEN '시공 완료'
ELSE b.status::varchar
END AS status_kr,

이하 생략

반응형
반응형

const geo_country = h.get('x-vercel-ip-country') || h.get('cf-ipcountry') || null

동작 방식

출처 헤더 조건
Vercel x-vercel-ip-country Vercel 배포 시 Edge Network가 자동 주입
Cloudflare cf-ipcountry Cloudflare 프록시 통과 시 자동 주입

vercel, cloudflare 외에 다른 환경이라면
AWS / Netlify / 자체 서버: 위 헤더들이 자동으로 안 와요. 그 경우엔 IP → 국가 변환을 직접 해야 합니다.

무료 옵션:

  • geoip-lite npm 패키지 (DB 파일 번들, 오프라인 lookup)
  • ipapi.co / ip-api.com 같은 외부 API (요청당 호출, rate limit 있음)
  • MaxMind GeoLite2 DB 다운로드해서 직접 lookup
반응형
반응형

deviceType: bot|mobile|tablet|desktop|unknown

UPDATE public.decozip_ad_visit
SET device_type = CASE
  WHEN user_agent IS NULL OR user_agent = '' THEN 'unknown'
  WHEN user_agent ~* 'bot|crawler|spider|crawling|facebookexternalhit|slackbot|whatsapp|kakaotalk-scrap'
    THEN 'bot'
  WHEN user_agent ~* 'iPhone|iPod|Android.*Mobile|Mobi|BlackBerry|IEMobile|Opera Mini|webOS'
    THEN 'mobile'
  WHEN user_agent ~* 'iPad|Tablet|Android(?!.*Mobile)|Kindle|Silk|PlayBook'
    THEN 'tablet'
  ELSE 'desktop'
END
WHERE device_type IS NULL;
export function detectDeviceType(ua: string | null | undefined): AdVisitDeviceType {
  if (!ua) return 'unknown'
  if (/bot|crawler|spider|crawling|facebookexternalhit|slackbot|whatsapp|kakaotalk-scrap/i.test(ua)) return 'bot'
  if (/iPhone|iPod|Android.*Mobile|Mobi|/**
 * user-agent 문자열에서 브라우저 종류를 추출.
 * 한국형 인앱(KAKAO/Instagram/FB/당근/Whale) 을 일반 브라우저보다 먼저 검사.
 * (Chrome UA 에 Safari 가 포함되므로 Safari 는 Chrome 보다 뒤에)
 * (data/migration_2026_06_25_ad_visit_browser_geo.sql 의 백필 규칙과 동일)
 */
export function detectBrowser(ua: string | null | undefined): AdVisitBrowser {
  if (!ua) return 'unknown'
  if (/KAKAOTALK/i.test(ua)) return 'kakao'
  if (/Instagram/i.test(ua)) return 'instagram'
  if (/FBAN|FBAV|FB_IAB/i.test(ua)) return 'facebook'
  if (/Daangn|DaangnApp/i.test(ua)) return 'daangn'
  if (/Whale\//i.test(ua)) return 'whale'
  if (/SamsungBrowser/i.test(ua)) return 'samsung'
  if (/Edg\/|EdgA\/|EdgiOS\//i.test(ua)) return 'edge'
  if (/Firefox\/|FxiOS\//i.test(ua)) return 'firefox'
  if (/Chrome\/|CriOS\//i.test(ua)) return 'chrome'
  if (/Safari\//i.test(ua)) return 'safari'
  return 'other'
}|Opera Mini|webOS/i.test(ua)) return 'mobile'
  if (/iPad|Tablet|Kindle|Silk|PlayBook/i.test(ua)) return 'tablet'
  if (/Android/i.test(ua) && !/Mobile/i.test(ua)) return 'tablet'
  return 'desktop'
}

browser:

UPDATE public.decozip_ad_visit
SET browser = CASE
  WHEN user_agent IS NULL OR user_agent = ''       THEN 'unknown'
  WHEN user_agent ~* 'KAKAOTALK'                   THEN 'kakao'
  WHEN user_agent ~* 'Instagram'                   THEN 'instagram'
  WHEN user_agent ~* 'FBAN|FBAV|FB_IAB'            THEN 'facebook'
  WHEN user_agent ~* 'Daangn|DaangnApp'            THEN 'daangn'
  WHEN user_agent ~* 'Whale/'                      THEN 'whale'
  WHEN user_agent ~* 'SamsungBrowser'              THEN 'samsung'
  WHEN user_agent ~* 'Edg/|EdgA/|EdgiOS/'          THEN 'edge'
  WHEN user_agent ~* 'Firefox/|FxiOS/'             THEN 'firefox'
  WHEN user_agent ~* 'Chrome/|CriOS/'              THEN 'chrome'
  WHEN user_agent ~* 'Safari/'                     THEN 'safari'
  ELSE 'other'
END
WHERE browser IS NULL;
/**
 * user-agent 문자열에서 브라우저 종류를 추출.
 * 한국형 인앱(KAKAO/Instagram/FB/당근/Whale) 을 일반 브라우저보다 먼저 검사.
 * (Chrome UA 에 Safari 가 포함되므로 Safari 는 Chrome 보다 뒤에)
 * (data/migration_2026_06_25_ad_visit_browser_geo.sql 의 백필 규칙과 동일)
 */
export function detectBrowser(ua: string | null | undefined): AdVisitBrowser {
  if (!ua) return 'unknown'
  if (/KAKAOTALK/i.test(ua)) return 'kakao'
  if (/Instagram/i.test(ua)) return 'instagram'
  if (/FBAN|FBAV|FB_IAB/i.test(ua)) return 'facebook'
  if (/Daangn|DaangnApp/i.test(ua)) return 'daangn'
  if (/Whale\//i.test(ua)) return 'whale'
  if (/SamsungBrowser/i.test(ua)) return 'samsung'
  if (/Edg\/|EdgA\/|EdgiOS\//i.test(ua)) return 'edge'
  if (/Firefox\/|FxiOS\//i.test(ua)) return 'firefox'
  if (/Chrome\/|CriOS\//i.test(ua)) return 'chrome'
  if (/Safari\//i.test(ua)) return 'safari'
  return 'other'
}

os

UPDATE public.decozip_ad_visit
SET os = CASE
  WHEN user_agent IS NULL OR user_agent = ''       THEN 'unknown'
  WHEN user_agent ~* 'Android'                     THEN 'android'
  WHEN user_agent ~* 'iPhone|iPad|iPod|iOS|CriOS|FxiOS|EdgiOS' THEN 'ios'
  WHEN user_agent ~* 'Windows NT|Windows Phone|WPDesktop' THEN 'windows'
  WHEN user_agent ~* 'Mac OS X|Macintosh'          THEN 'macos'
  WHEN user_agent ~* 'BlackBerry|BB10|RIM Tablet'  THEN 'blackberry'
  WHEN user_agent ~* 'Linux|X11'                   THEN 'linux'
  ELSE 'other'
END
WHERE os IS NULL;
export function detectBrowser(ua: string | null | undefined): AdVisitBrowser {
  if (!ua) return 'unknown'
  if (/KAKAOTALK/i.test(ua)) return 'kakao'
  if (/Instagram/i.test(ua)) return 'instagram'
  if (/FBAN|FBAV|FB_IAB/i.test(ua)) return 'facebook'
  if (/Daangn|DaangnApp/i.test(ua)) return 'daangn'
  if (/Whale\//i.test(ua)) return 'whale'
  if (/SamsungBrowser/i.test(ua)) return 'samsung'
  if (/Edg\/|EdgA\/|EdgiOS\//i.test(ua)) return 'edge'
  if (/Firefox\/|FxiOS\//i.test(ua)) return 'firefox'
  if (/Chrome\/|CriOS\//i.test(ua)) return 'chrome'
  if (/Safari\//i.test(ua)) return 'safari'
  return 'other'
}
반응형
반응형

timestamptz로 저장하면 아래와 같이 utc정보를 같이 저장하게 됨

+0900 -> 이거보고 한국시간 기준이라고 판단하는 게 좋을 듯

  • timestamp vs timestamptz
타임존 고려 ❌ (고려 안 함) ⭕ (UTC로 변환하여 저장)
저장 값 입력된 "문자열" 그대로 UTC 기준의 절대 시간
조회 시 어느 지역에서든 동일 조회하는 지역의 시간대로 자동 변환

DB내부적으로는 다 utc로 저장하되
조회할때는 지역시간대에 맞춰서 보여주는듯

 

반응형
반응형
  • Stop: 작업이 끝났을때

  • Notification: 작업하다가 뭔가를 물어본다든지 해서 중단되었을 때

  "hooks": {

    "Stop": \[

      {

        "hooks": \[

          {

            "type": "command",

            "shell": "powershell",

            "async": true,

            "command": "$f=\\"$env:WINDIR\\\\Media\\\\notify.wav\\"; if(Test-Path $f){(New-Object Media.SoundPlayer $f).PlaySync()}else{\[console\]::beep(800,300)}"

          }

        \]

      }

    \]

  }
반응형
반응형
  • default - 항상 묻기
  • acceptEdits — Edit/Write 자동 허용
  • auto — 더 광범위한 자동 모드 (Bash 등까지 일부 자동), but 위험한건 allow에 있어도 실행안함
  • bypassPermissions — 모든 권한 우회 (위험, 비추)
항목 acceptEdits auto
Edit / Write 항상 자동 허용 자동 허용
Read / Glob / Grep allow 규칙 따라감 자동 허용 (안전 도구)
Bash allow 규칙 따라감 (Bash(*) 가 있으면 통과) 분류기가 명령을 보고 판단 — 안전(ls, grep)은 통과, 위험(rm -rf, git push --force 등)은 차단/확인
PowerShell 같은 방식 같은 방식
위험한 명령 allow 패턴에 매칭만 되면 그대로 실행 자동 차단 (예: rm -rf *, chmod 777, 임의 fs 쓰기)

핵심 차이

  • acceptEdits = "파일 수정만 자동, 나머지는 allow 리스트 그대로"
    • 단순하고 예측 가능
    • allow 에 적힌 패턴이 위험해도 그대로 실행됨
  • auto = "LLM 분류기가 매 명령을 보고 판단"
    • 더 똑똑함 — Bash(*) 같이 광범위한 allow 규칙이 있어도 위험한 명령은 자동 차단
    • allow 리스트보다 더 안전한 가드레일 추가
    • 단점: 가끔 모호한 명령은 분류 위해 잠깐 멈출 수 있음

사용자 케이스 (현재 Bash(*) 광범위 허용 중)

지금 Bash(*) 가 모두 허용이라 rm -rf 같은 위험 명령도 무프롬프트로 실행됩니다.

  • acceptEdits 유지 → 현 상태와 같음. 빠르지만 보안 가드 없음.
  • auto 로 변경 → Edit/Write 는 똑같이 자동, 거기에 Bash 위험 명령은 자동 차단. 보안↑ 약간의 안전망.

추천

  • 속도 우선 + 본인이 명령 검토 신경 안 씀acceptEdits 유지
  • 보안 가드레일 원함 (특히 광범위한 Bash 허용 중)auto 권장

bypassPermissions설정시에는 "skipDangerousModePermissionPrompt": true 설정권장

{
  "permissions": {
    "allow": [
      "Bash(*)",
      "PowerShell(*)",
      "WebFetch(*)",
      "Edit(/.claude/skills/blog-draft/**)",
      "Edit(/.claude/skills/lead-bootstrap/**)",
      "Read(//tmp/**)"
    ],
    "defaultMode": "bypassPermissions",
    "additionalDirectories": [
      "c:\\dev\\lumane-blog\\content\\2026-05-07_인천-미추홀구_학익풍림아이원",
      "c:\\dev\\decozip",
      "C:\\Users\\samsung\\.claude"
    ]
  },
  "skipDangerousModePermissionPrompt": true
}
반응형
반응형

1) Git 자동 연동 방식

2) GitHub Actions(ci.yml)가 Vercel CLI를 직접 호출해서 배포하는 방식


설정 파일: .github/workflows/ci.yml

name: CI

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  check:
    name: Typecheck & Test
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: npm

      - name: Install dependencies
        run: npm ci

      - name: Typecheck
        run: npm run typecheck

      - name: Unit tests
        run: npm test

  deploy:
    name: Deploy to Vercel
    runs-on: ubuntu-latest
    needs: check
    timeout-minutes: 15

    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: npm

      - name: Install dependencies
        run: npm ci

      - name: Install Vercel CLI
        run: npm install -g vercel@latest

      - name: Pull Vercel environment
        run: vercel pull --yes --environment=${{ github.ref == 'refs/heads/main' && 'production' || 'preview' }} --token=${{ secrets.VERCEL_TOKEN }}
        env:
          VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
          VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}

      - name: Build
        run: vercel build ${{ github.ref == 'refs/heads/main' && '--prod' || '' }} --token=${{ secrets.VERCEL_TOKEN }}
        env:
          VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
          VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}
          NEXT_PUBLIC_SUPABASE_URL: ${{ vars.NEXT_PUBLIC_SUPABASE_URL }}
          NEXT_PUBLIC_SUPABASE_ANON_KEY: ${{ vars.NEXT_PUBLIC_SUPABASE_ANON_KEY }}

      - name: Deploy
        run: vercel deploy --prebuilt --yes ${{ github.ref == 'refs/heads/main' && '--prod' || '' }} --token=${{ secrets.VERCEL_TOKEN }}
        env:
          VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
          VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}

트리거 — 언제 실행되나

두 경우에 실행된다:

  • main 브랜치에 push 할 때
  • main을 향한 Pull Request가 열리거나 갱신될 때

다른 브랜치에 push만 하는 것으로는 실행되지 않는다. PR을 만들거나 main에 직접 push해야 동작한다.


절차 — 2개의 Job

1단계: check (검증)

  1. 코드 체크아웃 → Node 20 설치
  2. npm ci (의존성 설치)
  3. npm run typecheck (타입 검사)
  4. npm test (단위 테스트)

여기서 하나라도 실패하면 배포로 넘어가지 않는다.

2단계: deploy (배포)

needs: check 이므로 1단계가 모두 통과해야만 시작된다.

  1. 체크아웃 → Node 설치 → npm ci
  2. Vercel CLI 설치 (vercel@latest)
  3. vercel pull — Vercel에서 환경설정을 가져옴
  4. vercel build — 빌드
  5. vercel deploy --prebuilt — 빌드 결과물을 Vercel에 업로드/배포

Production vs Preview 구분

핵심은 github.ref == 'refs/heads/main' 조건이다.

상황 environment 빌드/배포 플래그 결과
main에 push production --prod 붙음 운영 배포 (실서비스 반영)
PR (main 대상) preview 플래그 없음 프리뷰 배포 (미리보기 URL)

PR을 올리면 프리뷰 배포, main에 머지/push하면 운영 배포가 된다.


필요한 비밀값 (GitHub Secrets / Variables)

이 워크플로우가 돌려면 GitHub 저장소 설정에 아래가 등록돼 있어야 한다.

  • Secrets: VERCEL_TOKEN, VERCEL_ORG_ID, VERCEL_PROJECT_ID
  • Variables (빌드 시 주입): NEXT_PUBLIC_SUPABASE_URL, NEXT_PUBLIC_SUPABASE_ANON_KEY

설정 위치: https://github.com/rumanesystem/lumane_crm/settings/secrets/actions


주의 — 중복 배포

이 방식은 Vercel 대시보드의 "Git 자동 연동(Connected Git Repository)"이 아니라, GitHub Actions가 CLI로 직접 배포하는 구조다.

Vercel 프로젝트에 Git 연동까지 동시에 켜져 있으면 같은 push에 배포가 2번 일어날 수 있다.

  • 대응: Vercel 프로젝트 → Settings → Git 에서 자동 배포를 끄거나 연결 해제한다.
  • 또는 반대로 ci.ymldeploy job을 지우고 Vercel 자동 연동만 쓴다. (둘 중 하나만)
반응형
반응형

1. 온라인 변환기 — 가장 빠름 (3초)

https://tablesgenerator.com/markdown_tables

  1. 엑셀에서 셀 범위 선택 → Ctrl+C
  2. 사이트 접속 → 상단 File 메뉴 → Paste table data 클릭
  3. 붙여넣기 → "Load" → 우측에 markdown 결과 자동 생성
  4. 복사해서 어디든 붙여넣기

회원가입·설치 불필요. 헤더·정렬·소수점 포맷 옵션도 다 있음.

2. VSCode 확장 — 반복 작업할 때

확장: "Excel to Markdown table" (작성자: csholmq)

  1. VSCode 확장 마켓플레이스에서 설치
  2. 엑셀에서 셀 복사
  3. VSCode 마크다운 파일에서 Ctrl+Shift+P → "Excel to Markdown table" → Enter
  4. 커서 위치에 markdown 표 삽입

엑셀 ↔ VSCode 왔다갔다 잦으면 이게 제일 편함.

3. 텍스트 에디터로 수동 변환 — 사이트 못 쓸 때

엑셀에서 복사한 데이터는 탭(\t)으로 구분된 텍스트입니다.

  1. 메모장/VSCode 에 붙여넣기
  2. Ctrl+H (찾기·바꾸기)
  3. 탭 문자(\t)를 | 로 일괄 치환
  4. 각 줄 시작·끝에 | 추가
  5. 헤더 줄 다음에 |---|---|---| 같은 구분 줄 끼우기

번거롭지만 인터넷 안 될 때 유용.


대량 데이터(수백 행 이상)거나 자동화 필요하면 Python 한 줄:

import pandas as pd
print(pd.read_excel("파일.xlsx").to_markdown(index=False))
반응형
반응형

.url(인터넷 바로가기)는 일반 파일 확장자 표시 설정과 다른 곳에서 제어됩니다. Windows가 레지스트리에 NeverShowExt라는 별도 플래그를 두고 무조건 숨기도록 강제하고 있어서, 폴더 옵션의 "파일 이름 확장명" 체크와 무관하게 항상 숨겨집니다. (같은 이유로 .lnk 바로가기 확장자도 안 보입니다.)

해결 방법: 레지스트리 수정 (관리자 권한 필요)

PowerShell로 한 번에 (권장)

관리자 권한 PowerShell에서:

# .url (인터넷 바로가기) 확장자 표시
Remove-ItemProperty -Path "Registry::HKEY_CLASSES_ROOT\InternetShortcut" -Name "NeverShowExt" -ErrorAction SilentlyContinue

# (선택) .lnk (일반 바로가기) 확장자도 같이 표시
Remove-ItemProperty -Path "Registry::HKEY_CLASSES_ROOT\lnkfile" -Name "NeverShowExt" -ErrorAction SilentlyContinue

# 적용을 위해 탐색기 재시작 - 실제로 해보니 꼭 재시작해야 적용됨
Stop-Process -Name explorer -Force

탐색기가 깜빡이며 재시작되고 나면 원본.md.url처럼 확장자가 보입니다.

레지스트리 편집기(GUI)로 하는 법

  1. Win + Rregedit 엔터 → 관리자 권한 허용
  2. 다음 경로로 이동:
    HKEY_CLASSES_ROOT\InternetShortcut
  3. 오른쪽 패널에서 NeverShowExt 항목 우클릭 → 삭제
  4. 작업 관리자에서 explorer.exe 우클릭 → 다시 시작 (또는 로그아웃/로그인)

되돌리는 법

원래대로 숨기고 싶다면:

New-ItemProperty -Path "Registry::HKEY_CLASSES_ROOT\InternetShortcut" -Name "NeverShowExt" -Value "" -PropertyType String -Force
Stop-Process -Name explorer -Force

보너스: 확장자 안 보이는 다른 파일 형식들

같은 NeverShowExt 플래그가 박혀 있는 대표적인 형식:

확장자 레지스트리 키 용도
.url HKCR\InternetShortcut 인터넷 바로가기
.lnk HKCR\lnkfile 일반 바로가기
.pif HKCR\piffile 프로그램 정보 파일(레거시)
.shs HKCR\shellscrap 셸 조각(레거시, 보안상 비활성)

.lnk까지 같이 풀어두면 바탕화면/시작메뉴 바로가기 파일도 확장자가 보이게 되니, 개발자 입장에선 일관성 있게 모두 푸는 걸 추천합니다.

반응형
반응형

Windows에서 Next.js Dev 환경 성능 개선 가이드

Windows + Next.js 16 + VSCode 환경에서 dev 서버와 TypeScript Server가 CPU·메모리를 과도하게 사용하는 문제를 해결하기 위한 5가지 방법.

영향 큰 순서: 1번 > 4번 > 3번 > 5번 > 2번


1. Windows Defender 실시간 스캔 제외 — 가장 효과 큼

왜 효과가 큰가

Defender는 파일이 읽히거나 쓰일 때마다 실시간 검사를 수행한다. node_modules는 파일이 수만 개, .next/cache는 빌드할 때마다 다시 쓰여서 dev 서버 시간의 30~50%를 Defender 검사가 차지하는 경우가 흔하다.

실행 방법 (관리자 권한 PowerShell)

Windows 시작 → "PowerShell" 검색 → 우클릭 → "관리자 권한으로 실행"

# 1) 프로젝트 폴더 제외
Add-MpPreference -ExclusionPath "c:\dev\decozip"

# 2) Node.js 프로세스 제외
Add-MpPreference -ExclusionProcess "node.exe"

# (선택) 3) npm 글로벌 폴더도 제외하면 npm install 도 빨라짐
Add-MpPreference -ExclusionPath "$env:APPDATA\npm"
Add-MpPreference -ExclusionPath "$env:APPDATA\npm-cache"

확인 방법

Get-MpPreference | Select-Object -ExpandProperty ExclusionPath
Get-MpPreference | Select-Object -ExpandProperty ExclusionProcess

방금 추가한 경로/프로세스가 보이면 성공.

되돌리는 법

Remove-MpPreference -ExclusionPath "c:\dev\decozip"
Remove-MpPreference -ExclusionProcess "node.exe"

주의

  • 회사 PC는 IT 정책으로 막혀 있을 수 있음 (Add-MpPreference 호출 시 권한 에러).
  • 보안상 "신뢰할 수 있는 코드만 들어가는 폴더"만 제외할 것.

2. 좀비 Node 프로세스 정리

왜 필요한가

npm run dev 실행 후 터미널 X 버튼이나 VSCode 창을 그냥 닫으면 자식 node 프로세스가 백그라운드에 남는다. 누적되면 메모리 2~4GB씩 잡고 있어도 모를 수 있다.

현재 상태 확인 (일반 PowerShell)

Get-Process node -ErrorAction SilentlyContinue |
  Select-Object Id, @{n='MemMB';e={[math]::Round($_.WS/1MB,1)}}, CPU, StartTime |
  Sort-Object MemMB -Descending

출력 예시:

   Id MemMB    CPU StartTime
   -- -----    --- ---------
12345  892.3 145.2 2026-06-10 09:15:22  ← 살아있는 dev 서버
67890 1340.1 320.0 2026-06-08 11:20:01  ← 이틀 전 좀비!

StartTime이 며칠 전이면 좀비.

좀비 죽이기

# 전부 죽이기 (현재 실행 중인 dev 서버도 같이 죽음 — 주의)
Stop-Process -Name node -Force

# 특정 PID만 죽이기
Stop-Process -Id 67890 -Force

습관 만들기

  • dev 서버 종료는 항상 터미널에서 Ctrl+C.
  • 하루 작업 끝낼 때 Get-Process node 한 번 돌려서 확인.

3. Dev 서버 메모리 상한 설정

왜 필요한가

Next.js dev 서버는 시간이 지나면서 메모리가 야금야금 쌓인다 (HMR 캐시, 모듈 그래프). 상한을 두면 메모리가 너무 많이 차기 전에 OOM으로 죽고, 그때 재시작하면 깔끔해진다.

실행 방법

cross-env 패키지 설치 (Windows에서 환경변수 inline 설정용):

cd c:\dev\decozip\frontend
npm install -D cross-env

package.jsonscripts.dev 수정:

"scripts": {
  "dev": "cross-env NODE_OPTIONS=--max-old-space-size=2048 next dev",
  "build": "next build",
  "start": "next start",
  "lint": "eslint"
}
  • 2048 = 2GB. PC RAM이 16GB 이상이면 3072(3GB)도 OK.
  • 너무 낮게 (예: 1024) 잡으면 빌드 중에도 죽으니 2048~3072 권장.

동작 확인

dev 서버 띄우고 작업하다가 메모리 한계 도달 시 콘솔에 다음과 같이 뜸:

<--- Last few GCs --->
FATAL ERROR: Reached heap limit Allocation failed - JavaScript heap out of memory

이때 그냥 npm run dev 다시 실행하면 됨.

cross-env 없이 하는 법

PowerShell에서만 쓴다면:

$env:NODE_OPTIONS = "--max-old-space-size=2048"
npm run dev

(터미널 닫으면 사라지니 매번 입력해야 함 → cross-env 권장)


4. VSCode TypeScript Server 메모리 제한

왜 필요한가

VSCode가 별도로 띄우는 tsserver 프로세스가 큰 프로젝트에서 2~3GB 차지한다. dev 서버와 별개로 동작하므로 따로 제한해야 한다.

실행 방법

프로젝트 루트(c:\dev\decozip)에 .vscode/settings.json 파일이 있는지 확인. 없으면 만들고, 있으면 다음 항목을 추가:

{
  "typescript.tsserver.maxTsServerMemory": 2048,
  "typescript.tsserver.watchOptions": {
    "watchFile": "useFsEvents",
    "watchDirectory": "useFsEvents",
    "fallbackPolling": "dynamicPriority"
  },
  "files.watcherExclude": {
    "**/node_modules/**": true,
    "**/.next/**": true,
    "**/dist/**": true,
    "**/build/**": true
  },
  "search.exclude": {
    "**/node_modules": true,
    "**/.next": true
  }
}
  • maxTsServerMemory: tsserver 메모리 상한 (MB).
  • watchOptions: Windows에서 파일 감시 방식을 가벼운 것으로.
  • files.watcherExclude: node_modules.next 변화를 감시 대상에서 제외 (가장 큰 효과).

적용 확인

VSCode 우하단 상태바의 TypeScript 5.x.x 클릭 → "Restart TS Server" 한 번 누르면 새 설정으로 재시작.

작업 관리자에서 Code.exe / tsserver.exe 메모리 사용량 감소 확인.

TS Server가 갑자기 느려질 때

명령 팔레트(Ctrl+Shift+P) → "TypeScript: Restart TS Server" → 즉시 회복.


5. 주기적 dev 서버 + .next 폴더 청소

왜 필요한가

.next 폴더는 dev 빌드 캐시·HMR 청크·소스맵을 누적한다. 며칠 작업하면 500MB~2GB까지 부풀어 디스크 I/O가 느려진다.

작업 끝낼 때 청소 (수동)

cd c:\dev\decozip\frontend
# dev 서버 먼저 종료 (Ctrl+C 또는 Stop-Process)
Remove-Item -Recurse -Force .next

청소 스크립트로 만들기

package.json:

"scripts": {
  "dev": "cross-env NODE_OPTIONS=--max-old-space-size=2048 next dev",
  "clean": "rimraf .next",
  "dev:fresh": "npm run clean && npm run dev",
  "build": "next build",
  "start": "next start",
  "lint": "eslint"
}

rimraf 설치:

npm install -D rimraf

이제 깔끔하게 시작하고 싶을 때:

npm run dev:fresh

작업 패턴 권장

  • 평소: npm run dev 한 번 띄워두고 계속 사용
  • 메모리/CPU 무거워지면: Ctrl+C → npm run dev (10초)
  • 며칠에 한 번: npm run dev:fresh (캐시 청소)
  • 일주일에 한 번: Get-Process node로 좀비 점검

적용 순서 추천

순서 작업 권한 소요 시간
1순위 Defender 제외 관리자 PowerShell 1분
2순위 VSCode 설정 (.vscode/settings.json) 일반 2분
3순위 메모리 상한 + clean 스크립트 일반 5분
습관 좀비 점검 일반 주 1회

1번(Defender) + 4번(VSCode 설정) 두 가지만 적용해도 체감 차이가 가장 크다.

반응형
반응형

이미지에서 중요한 사실이 보입니다. 사용자님 진단(vercel pull 문제)도 일리 있지만, 진짜 핵심은 배포 경로가 중복돼 있다는 점입니다. 체크 항목을 하나씩 보겠습니다.

이미지가 말해주는 것

체크 항목 상태 정체
CI / Deploy to Vercel (push) 🔄 3시간째 In progress ci.yml의 deploy job (GitHub Actions가 CLI로 배포)
CI / Typecheck & Test (push) ✅ 1분 성공 ci.yml의 check job
Vercel ✅ Deployment has completed Vercel 자체 Git 자동 연동 (ci.yml과 별개!)
helpful-growth - lumane_crm ✅ Success → railway.app Railway 자동 배포 (또 다른 경로)

→ 즉, 제가 지난번에 경고했던 "중복 배포"가 실제로 일어나고 있습니다. 배포 경로가 3개예요:

  1. ci.yml의 Deploy 잡 (← 지금 멈춰있음)
  2. Vercel 자체 Git 연동 (← 이미 배포 완료 ✅)
  3. Railway 연동 (← 이미 성공 ✅)

그래서 실제 서비스는 이미 정상 배포된 상태입니다. ci.yml의 deploy 잡만 따로 매달려 있는 것뿐입니다.

왜 3시간이나 멈춰 있나

ci.yml:35timeout-minutes: 15가 있어서 원래는 15분 후 자동 실패해야 합니다. 3시간째 "In progress"라면 실행 자체가 안 되고 러너 배정 대기 중이거나 상태가 멈춘(stuck) 것일 가능성이 큽니다. 정확한 원인은 실제 로그를 봐야 알 수 있고, 그러려면 gh CLI가 필요합니다(현재 미설치).

권장 방향

Vercel 자체 연동이 이미 잘 배포하고 있으니, ci.yml의 deploy 잡을 제거하고 typecheck/test 검증만 남기는 것이 가장 깔끔합니다. 중복 배포와 hang이 한 번에 사라집니다.

CI / Deploy to Vercel (push) In progress - This check has started...

CI / Typecheck & Test (push) Successful in 1m

Vercel - Deployment has completed

helpful-growth - lumane_crm - Success - lumanecrm-production.up.railway.app

반응형
반응형

Claude Code 자체에는 사용자의 메시지 색만 따로 바꾸는 설정이 없음

  • /color 원하는색 같은 명령어는 존재하지 않음 → "isn't available" 메시지가 정상 동작
  • settings.jsontheme.userMessageColor, colors.user 같은 키 없음
  • /theme·/config theme은 전체 테마만 바꾸지, "내 메시지만" 못 바꿈
  • 훅·status line·ANSI escape 같은 우회로도 메시지 표시 UI에는 적용되지 않음

원천 솔루션이 필요하면 Claude Code 안에서 /feedback으로 기능 요청.

대안: VSCode workbench.colorCustomizations로 우회

1단계: VSCode settings.json 열기

명령 팔레트(Ctrl+Shift+P) → Preferences: Open User Settings (JSON)

Windows 기본 경로:

C:\Users\<사용자>\AppData\Roaming\Code\User\settings.json

2단계: workbench.colorCustomizations 블록 추가

 "workbench.colorCustomizations": {
        //"input.background" 입력한 메세지의 배경색
        //"input.background":"#364fc7",//짙은 인디고 https://yeun.github.io/open-color/ 색상선택시
        //"input.background":"#97bfb4",//차분한 민트/세이지 톤
        "input.background":"#1d3b35",//짙은 포레스트
        //"input.background": "#ffe4ef",//핑크
        //"input.foreground" 내가 타이핑하는 글자
        //"input.foreground": "#5a1d3b",//갈색
        //"input.border": "#ff7eb6",//입력칸 테두리
        //"editor.selectionBackground": "#ffd1e8",
        //"input.placeholderForeground" 안내 문구(placeholder)
        //"input.placeholderForeground":"#3a5550",//짙은 포레스트와 비슷
  },

저장 즉시 반영 (재시작 불필요).

3단계: 특정 테마에만 적용 (선택)

전체에 영향 주기 싫으면 테마 이름으로 감싸기:

{
  "workbench.colorCustomizations": {
    "[Default Dark Modern]": {
      "input.background": "#97bfb4",
      "input.foreground": "#1a1a1a"
    }
  }
}

적용 후에도 안 바뀌면 input.background 등의 이름이 안맞아일수 있음 — DevTools로 실제 색 변수 찾기

직접 찾는 절차:

  1. Help → Toggle Developer Tools -선택하면 크롬 개발자 도구처럼 생긴게 나옴

  2. 좌상단 요소 선택 아이콘 클릭

  3. 자기 메시지/입력칸을 클릭

  4. 우측 Styles 패널에서 그 요소가 쓰는 CSS 변수 확인

    background-color: var(--vscode-chat-requestBackground);
    color: var(--vscode-foreground);
    border: 1px solid var(--vscode-input-border);
  5. 변수명에서 --vscode-를 떼면 그게 workbench.colorCustomizations의 키 이름

매핑 예시:

CSS 변수 settings.json 키
--vscode-input-background input.background
--vscode-textBlockQuote-background textBlockQuote.background
--vscode-editorWidget-background editorWidget.background

같은 화면에 보이는 색이라도:

  • var(--vscode-…) 변수로 받는 색 → settings.json으로 바꿀 수 있음 ✅
  • 직접 #xxxxxx 박혀 있는 색 → 못 바꿈 ❌

  • 그래도 안 되면 /feedback으로 기능 요청 해야함
반응형
반응형

 

Uncaught TypeError: Failed to execute 'measure' on 'Performance': '​CaptainEntry' cannot have a negative time stamp. at flushComponentPerformance (react-server-dom-turbopack-client.browser.development.js:3846:35) at flushComponentPerformance (react-server-dom-turbopack-client.browser.development.js:3761:27) at flushInitialRenderPerformance (react-server-dom-turbopack-client.browser.development.js:4207:11)

이 에러는 Next.js 16 + Turbopack의 알려진 문제로, redirect()를 동기 함수에서 호출하는 Server Component에서 React 성능 측정이 실패하는 버그입니다. 기능 자체는 정상 동작하지만 콘솔에 에러가 표시됩니다.

원인은 app/captain/page.tsx의 CaptainEntry 함수가 동기인 점입니다. async로 바꾸면 해결됩니다.

// async 함수로 작성: Next.js 16 + Turbopack의 sync redirect 성능 측정 버그 회피

export default function  -> export default async function 

import { redirect } from 'next/navigation'

// PoC: 인증 없이 첫 방장(seq=1)으로 진입
// 향후 Supabase Auth 도입 후 현재 로그인 사용자의 captain_seq로 redirect
// async 함수로 작성: Next.js 16 + Turbopack의 sync redirect 성능 측정 버그 회피
export default async function CaptainEntry() {
  redirect('/captain/1')
}

 

반응형
반응형

반응형
반응형

decozip-21d71bbok-rumane-s-projects.vercel.app

21d71bbok특정 배포 인스턴스의 해시

반응형
반응형

git log -5(최근 커밋 5개 확인)로 보면 클로드가 한 커밋은 이렇게 남음

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

git hub에서는 클로드와 config name으로 설정한 커밋터가 공동 커밋한 것처럼 표시됨

반응형
반응형

git commit할때는 Windows 자격증명 없어도 됨

근데 commit할때 무조건 name/email은 필요함 - 이건 git config user.name, git config user.email로 설정확인가능

그리고 git log -1 명령어를 쓰면 최근  commit에 대해 어떤 name/email로 커밋되었는지 확인가능함

반응형
반응형

git log --oneline -5

2e2dae3 (HEAD -> main) ad.html 슬라이드 16,18-22 삭제 (16슬라이드로 축소)
d962011 (origin/main, origin/HEAD) Decozip 홍보 이미지 HTML 추가
48c3b03 presentation.html -> ad.html
b93a3e6 프레젠테이션 HTML public 폴더에 추가
8b3f1e8 .claude/ 폴더 gitignore 추가

git status

 

git commit -a

git add 하지 않고 바로 커밋가능, but 새로추가된 파일이 있다면 따로 add해줘야함

 

git commit --amend

그전 커밋에 붙여서 커밋

 

git tag myTag 9fceb02: 특정 커밋에 태그달기

git push --tag: remote에 push

 

git checkout 가고싶은브랜치

 

git merge 과정

1 git checkout main main 으로 이동 (병합 받을 쪽)
2 git pull 원격 main 최신화
3 git merge point_incentive point_incentive 의 커밋을 main 으로 가져옴
4 git push origin main main 원격에 반영
5 git branch -d point_incentive ← 이게 찾으시던 그 명령. 로컬 브랜치 삭제
6 git push origin --delete point_incentive 원격(GitHub)의 브랜치도 삭제

git merge와 동일한 효과내는 것, git rebase (=git cherry-pick을 여러번 하는 것) 

git rebase, git cherry-pick하면 commit history가 더 깔끔

반응형
반응형

부분캡쳐(Shift+Windows+s)를 하면 클립보드에 저장이 되고 파일 자동 다운로드 되지는 않는다.

그러나 윈도우즈 temporary 파일시스템 어딘가에 자동자동이 되고는 있다. 파악하려면  everything 프로그램을 설치하고 캡쳐후에 바로 생성되는 파일을 추척하면 알수 있다. everything  프로그램은 가벼워서 깔아놓고 윈도우내 임시 파일 위치 찾기에 괜찮네

실제로 저장되는 위치는 찾아봤더니 아래와 같았다. 웹검색 결과에서 말하는 경로와는 달라서 찾기 어려웠음

루마네 데스크탑 C:\Users\samsung\AppData\Local\Packages\Microsoft.ScreenSketch_8wekyb3d8bbwe\TempState\Snips

 

한성 8G램 i5 64bit Windows10Home 노트북 C:\Users\family\AppData\Local\Packages\MicrosoftWindows.Client.CBS_cw5n1h2txyewy\TempState\ScreenClip

 

반응형
반응형

예전에는 그랬는데, 요즘은 선택지가 많아졌어요.

📘 쉬운 설명: 한때 "한국에서 치킨 배달 = 전화 주문"이었는데, 지금은 배달의민족·쿠팡이츠 등 다양해진 것처럼요. Express가 오랫동안 표준이었지만 지금은 상황에 따라 달라요.


현실적인 점유율

  • 소규모 프로젝트 / 스타트업 초기 → Express 아직 압도적 다수
  • 대규모 기업 → NestJS 비율이 빠르게 늘고 있음
  • 최신 트렌드 → Fastify, Hono 늘어나는 중

왜 아직도 Express가 많냐면

  1. 10년 이상 쌓인 레퍼런스 — 구글에서 에러 치면 답이 다 나옴
  2. 진입 장벽이 낮음 — 이 프로젝트처럼 20줄이면 서버가 뜸
  3. npm 생태계 호환 — 웬만한 라이브러리는 Express 기준으로 만들어짐
  4. 관성 — 팀이 이미 알고 있으니 굳이 바꿀 이유가 없음

Express 안 쓰는 대표적인 경우

상황대안이유

팀이 크고 구조가 중요 NestJS 폴더 구조·의존성 주입 강제 — 협업에 유리
성능이 중요 Fastify Express보다 2~3배 빠름
Edge/서버리스 환경 Hono Cloudflare Workers, Vercel Edge 등에서 작동
아예 Node.js 탈출 Deno + Oak, Bun + Elysia 새로운 런타임 자체를 씀

📘 쉬운 설명:

  • NestJS는 "규칙이 엄격한 대기업 사무실", Express는 "자유로운 스타트업 사무실"이에요.
  • 사람 적을 땐 자유로운 게 빠르지만, 사람 많아지면 규칙이 있어야 안 헷갈리죠.

이 프로젝트에 Express가 맞는 이유

  • 1인 운영, 빠른 수정이 중요
  • 2,200줄 정도 규모면 NestJS의 복잡한 구조가 오히려 부담
  • 기존 코드가 Express 기준으로 잘 짜여 있음

요약

"Node.js 백엔드 = Express"는 틀린 말은 아니지만, 점점 덜 맞아가는 말이에요.

입문할 때는 Express로 시작하는 게 맞고, 이 프로젝트 규모에서는 Express가 최선의 선택입니다. 다만 2024년 이후 신규 프로젝트에서는 Fastify나 Hono를 쓰는 경우도 꽤 보이기 시작했어요.

반응형
반응형

🔒 RLS (Row Level Security) — 행 단위 보안

PostgreSQL의 핵심 보안 기능으로, "누가 어떤 행(row)을 보거나 수정할 수 있는지"를 DB 자체가 판단해서 막아주는 장치예요.

📘 쉬운 설명: 도서관에 책이 1만 권 있는데, 손님마다 "당신은 이 책만 볼 수 있어요"라고 사서가 자동으로 골라주는 거예요. 손님이 "전체 책 목록 보여줘"라고 요청해도, 사서가 그 사람이 볼 수 있는 책만 추려서 보여줍니다. DB가 자체적으로 필터링해 주는 거죠.


1. RLS 없이 — 일반적인 권한 모델

기존 DB는 보통 테이블 단위 권한이에요.

사용자 A: quotes 테이블 읽기 권한 O → 전체 견적 다 보임
사용자 B: quotes 테이블 읽기 권한 X → 아무것도 못 봄

→ "A 손님은 본인 견적만 보고 싶다"는 요구사항을 DB 권한만으로는 표현 못 함. 그래서 보통 백엔드 코드에서 필터링해요:

// 백엔드 코드
const userId = 현재로그인한사용자ID;
await supabase.from('quotes').select('*').eq('user_id', userId);
//                                         ↑ 여기서 직접 필터

2. RLS 있으면 — 정책을 DB에 박아둠

-- DB에 정책 한 번 설정
CREATE POLICY "본인 견적만 조회"
ON quotes
FOR SELECT
USING (user_id = auth.uid());

이제 어떤 코드가 SELECT * FROM quotes를 날려도:

DB가 자동으로 → WHERE user_id = 현재사용자

를 붙여줍니다. 백엔드 코드의 실수와 무관하게 DB가 막아줘요.

📘 쉬운 설명: 백엔드 개발자가 깜빡하고 .eq('user_id', ...) 빼먹어도, DB가 알아서 다른 사람 거는 안 보여줘요. 2중 잠금이에요.


3. 왜 Supabase 직결과 RLS가 짝꿍인가

브라우저가 DB에 직접 접속하면, 누군가 개발자 도구에서 이런 코드를 날릴 수 있어요:

// 악의적 사용자가 콘솔에서 시도
await supabase.from('quotes').select('*'); 
// → 전체 견적 다 가져가려는 시도

이때 RLS가 없으면 → 다른 손님 견적 다 노출. RLS가 있으면 → 본인 것만 반환, 자동 필터링.

📘 쉬운 설명: 백엔드 없이 손님이 창고에 직접 들어가는 구조에서는, CCTV·자동문(RLS) 이 유일한 보호 장치예요. 이게 없으면 누구나 모든 데이터 가져갈 수 있어요.


4. 실제 정책 예시

예시 1: 본인 것만 조회

CREATE POLICY "own quotes only"
ON quotes FOR SELECT
USING (user_id = auth.uid());

→ 로그인한 사용자는 본인 견적만 조회 가능.

예시 2: 본인 것만 수정

CREATE POLICY "own quotes update"
ON quotes FOR UPDATE
USING (user_id = auth.uid());

→ 다른 사람 견적 수정 시도 시 무시됨.

예시 3: 어드민은 전체 조회

CREATE POLICY "admin sees all"
ON quotes FOR SELECT
USING (auth.jwt() ->> 'role' = 'admin');

→ JWT 토큰에 role: admin이 있으면 전체 보임.

예시 4: 공개 게시글

CREATE POLICY "public posts visible"
ON posts FOR SELECT
USING (is_public = true);

→ is_public = true인 행만 누구나 조회 가능.


5. 동작 흐름

1. 브라우저: SELECT * FROM quotes
       ↓
2. Supabase API: JWT 토큰에서 user_id 추출 → DB에 전달
       ↓
3. PostgreSQL:
   - RLS 정책 확인 → USING (user_id = auth.uid())
   - 자동으로 WHERE user_id = '추출된ID' 추가
       ↓
4. 결과: 본인 행만 반환

📘 쉬운 설명: 브라우저는 "전부 다 줘"라고 했지만, DB가 자기 판단으로 "당신 건 이거예요"만 골라서 줘요.


6. RLS의 강점

강점의미

코드 실수 방어 백엔드/프론트가 필터 잊어도 DB가 막음
중앙 집중 관리 권한 규칙이 DB 한 곳에 모임 (코드 흩어짐 X)
감사·검토 용이 정책 목록만 보면 "누가 뭘 볼 수 있나" 즉시 파악
백엔드 없이 안전 Supabase 직결 구조의 핵심 안전장치

7. RLS의 단점 / 함정

⚠️ 정책 설계 어려움

  • 사용자·어드민·게스트·소유자 등 역할별로 SELECT/INSERT/UPDATE/DELETE 각각 정책 필요
  • 빠뜨리면 → 데이터 노출
  • 잘못 짜면 → 정상 사용자가 데이터 못 봄

⚠️ 디버깅 까다로움

  • 정책에 막히면 그냥 "데이터 없음"으로 나옴
  • 에러 메시지가 없어서 "왜 안 보이지?" 찾기 힘듦

⚠️ 성능 영향

  • 모든 쿼리에 정책 조건이 자동 추가 → 인덱스 잘못 짜면 느려짐

⚠️ 활성화 필수

ALTER TABLE quotes ENABLE ROW LEVEL SECURITY;

이걸 안 하면 정책 만들어도 적용 안 됨. Supabase 초보가 가장 많이 하는 실수.

📘 쉬운 설명: CCTV를 설치만 하고 전원을 안 켠 거랑 같아요. 활성화를 깜빡하면 정책이 있어도 그냥 무방비.


8. 이 프로젝트는 RLS를 쓸까?

현재 이 프로젝트는 백엔드(Express)가 있고 SUPABASE_SECRET_KEY(service_role 키)를 써요. 이 키는 RLS를 우회합니다.

// server.js:46-50
const supabase = createClient(
  process.env.SUPABASE_URL,
  process.env.SUPABASE_SECRET_KEY,  // ← 이 키는 RLS 무시
  ...
);

📘 쉬운 설명: 백엔드 서버는 마스터 키를 갖고 있어요. RLS 정책이 있든 없든 다 통과해서 데이터 다 봅니다. 대신 백엔드 코드 안에서 requireAdmin 같은 자체 인증으로 권한을 통제하죠.

그래서 이 프로젝트는 RLS가 (있더라도) 핵심 방어선이 아님. 백엔드 코드가 방어선.

만약 어드민 페이지를 Supabase 직결로 바꾼다면, 그때부터는 RLS 정책 설계가 핵심이 됩니다.


9. Supabase의 두 가지 키 (이해 핵심)

키용도RLS 적용

ANON_KEY (anon) 브라우저용, 공개 가능 ✅ 적용 — 정책 따름
SERVICE_ROLE_KEY 백엔드 전용, 절대 노출 X ❌ 우회 — 다 됨

→ 직결 구조면 ANON_KEY + RLS 조합
→ 백엔드 구조면 SERVICE_ROLE_KEY + 백엔드 인증 (현재 이 프로젝트)

📘 쉬운 설명: 손님용 열쇠는 잠금장치(RLS) 따라 일부 방만 열려요. 직원용 마스터 키는 다 열리지만 직원만 갖고 있어야 해요.


요약

RLS = "DB가 직접 판단해서 행 단위로 접근을 막는 정책"

  • 핵심 가치: 백엔드 코드의 실수와 무관하게 DB가 안전 보장
  • 가장 빛나는 상황: Supabase 직결 구조 (백엔드 없음)
  • 단점: 정책 설계·디버깅 어려움, 활성화 깜빡 위험
  • 이 프로젝트: 백엔드가 마스터 키 쓰므로 RLS는 옵션 (필수 아님)

→ Supabase의 진짜 차별점 중 하나가 "PG의 RLS를 프론트엔드 직결 안전장치로 끌어다 쓸 수 있게 SDK·Auth와 통합 설계한 점" 입니다.

반응형
반응형

🟢 Supabase를 쓰면 좋은 점

이 프로젝트가 실제로 누리고 있는 이점을 중심으로 설명드릴게요.

📘 쉬운 설명: Supabase는 "DB + 인증 + 파일 저장 + 실시간 기능"을 한 번에 묶어 파는 서비스예요. 직접 DB 서버 구축하고 관리할 필요 없이, 회원가입하고 키 발급받으면 바로 쓸 수 있는 클라우드 DB라고 생각하시면 됩니다.


1. 서버 운영 부담이 거의 없다

기존 방식(직접 PostgreSQL 운영):

  • DB 서버 임대 → 설치 → 백업 자동화 → 보안 패치 → 장애 모니터링 → … 직접 다 해야 함

Supabase:

  • 회원가입 → URL/KEY 받기 → 끝
  • 백업, 업데이트, 장애 복구 다 Supabase가 알아서 함

이 프로젝트도 server.js:46-50에서 URL + KEY 2줄로 연결 끝났죠.

📘 쉬운 설명: DB를 직접 관리하려면 "서버 죽으면 누가 새벽에 일어나서 살려?" 같은 걱정이 따라와요. Supabase는 그걸 대신 해 줍니다.


2. PostgreSQL 그대로 = 한글 테이블명·복잡 쿼리 다 됨

Supabase는 내부적으로 진짜 PostgreSQL이라 표준 SQL 100% 호환이에요.

  • 이 프로젝트도 견적접수 같은 한글 테이블명을 그대로 씀
  • 스키마 분리(public + customer)도 PostgreSQL의 스키마 기능 그대로 활용 (server.js:46-57)

다른 NoSQL 서비스(Firebase 등)였다면 한글 테이블명·복잡한 JOIN·트랜잭션이 어려웠을 거예요.


3. 자바스크립트에서 SQL 안 짜고 데이터 쓰기/읽기

// SQL 안 쓰고 이렇게 끝
await supabase.from('quotes').upsert(payload, { onConflict: 'quote_number' });
await supabase.from('conversations').select('*').is('deleted_at', null);

server.js:311, server.js:1557 같은 코드가 그 예시.

📘 쉬운 설명: 보통 DB에 데이터 넣으려면 INSERT INTO quotes (...) VALUES (...) 같은 SQL을 작성해야 해요. Supabase는 자바스크립트 문법으로 그냥 .upsert(데이터) 한 줄이면 됩니다. SQL을 몰라도 DB를 쓸 수 있어요.


4. 스키마 분리로 개인정보 격리가 쉬움

이 프로젝트의 가장 큰 이점:

  • public 스키마 → 대화·견적·통계
  • customer 스키마 → 이름·전화·주소 (개인정보)

server.js:53-57에서 클라이언트를 분리해서, 환경변수 DB_SCHEMA로 어느 쪽 권한을 줄지도 통제 가능. 운영 정책상 "개인정보 접근은 별도 권한"을 코드 수준에서 강제하기 쉬워요.

📘 쉬운 설명: 손님 이름·전화번호 같은 민감한 정보를 별도 서랍에 넣어두고, 그 서랍은 특별한 열쇠가 있어야만 열리도록 만든 거예요. 정보 유출 사고가 나도 피해 범위가 줄어들죠.


5. 어드민 페이지에서 데이터 직접 보기

Supabase 대시보드(웹)에서 모든 테이블을 엑셀처럼 열어볼 수 있어요.

  • 상담 내용이 잘 저장됐는지 즉시 확인
  • 견적 한 줄 직접 수정 가능
  • SQL 에디터로 통계 쿼리도 즉석에서

📘 쉬운 설명: "DB에 뭐가 들어있지?" 궁금할 때 코드 안 짜고 웹사이트 들어가서 표 형태로 바로 볼 수 있어요. 어드민 페이지를 따로 안 만들어도 됩니다.


6. 무료 티어가 후함

  • DB 500MB
  • 월 50,000 활성 사용자
  • 자동 백업 (Pro부터는 더 길게)

작은~중간 규모 상담 서비스는 무료로도 충분히 굴러갑니다.


7. 이 프로젝트에서 안 쓰지만 곁들이 기능들

기능무엇

Auth 회원가입·로그인·소셜로그인 빌트인
Storage S3 같은 파일 저장소 (이미지 업로드용)
Realtime DB 변경을 WebSocket으로 실시간 푸시
Edge Functions 서버리스 함수 (작은 API용)

지금은 Express 서버 따로 두고 DB만 쓰지만, 필요하면 위 기능들을 점진적으로 끌어다 쓸 수 있어요.

📘 쉬운 설명: 예를 들어 어드민이 상담 페이지를 켜놨을 때, 새 손님 메시지가 들어오면 새로고침 없이도 화면에 뜨게 만드는 기능(Realtime)이 Supabase에 이미 있어요. 지금은 안 쓰지만, 필요해지면 바로 붙일 수 있습니다.


⚠️ 단점도 같이

공정하게:

  • 외부 서비스 의존 — Supabase가 장애 나면 우리 서비스도 멈춤 (운영메모/모니터링_알림.md에서 DB저장실패 알림이 있는 이유)
  • 트래픽 폭증 시 비용 — 무료→유료 전환 비용 발생
  • 자체 호스팅 가능하긴 한데 그러면 운영 부담의 장점이 사라짐

요약

이 프로젝트에 가장 큰 이득 3가지:

  1. DB 운영 안 해도 됨 — 코드만 짜면 됨
  2. PostgreSQL 그대로 — 한글 테이블명·스키마 분리(public/customer)로 개인정보 격리 깔끔
  3. JS SDK — SQL 안 짜고 .from().upsert() 한 줄로 끝

→ 1인~소규모 운영 환경에서 "DBA(데이터베이스 관리자) 없이도 운영 가능한 DB" 를 얻는 게 가장 큰 가치입니다.

반응형

+ Recent posts