어댑터

Prisma 어댑터

프로덕션용 어댑터 — 데이터베이스를 뒷단으로 삼는 HTTP 핸들러로, 인증, CORS, 정보 은닉(redaction), 웹훅을 지원합니다.

@instafix/adapter-prisma는 여러분 앱의 URL 하나를 완전한 피드백 API로 바꿔줍니다. Prisma Client v5, v6, v7을 모두 지원하며 Node 20 이상이 필요합니다.

npm install github:gnoopy/instafix#adapter-prisma-dist

마운트하기

이 팩토리 함수는 HTTP 메서드마다 하나씩, Web 표준 핸들러(RequestResponse)를 반환합니다 — 이를 마운트하는 방법은 여러분이 사용하는 프레임워크에 달려 있습니다:

// app/api/instafix/route.ts — Next.js App Router
import { createInstaFixHandler } from "@instafix/adapter-prisma";
import { prisma } from "@/lib/prisma";

export const { GET, POST, PATCH, DELETE, OPTIONS } = createInstaFixHandler({ prisma });

Web Request/Response를 사용하는 프레임워크라면(Hono, Remix, SvelteKit 등) 어떤 것이든 동일한 핸들러들을 하나의 엔드포인트에 마운트하고 메서드별로 디스패치할 수 있습니다.

옵션

옵션타입기본값하는 일
prismaInstaFixPrismaClient여러분의 Prisma 클라이언트. store를 전달하지 않는 한 필수입니다
storeInstaFixStorePrisma 대신 임의의 스토어를 사용합니다(지정 시 우선 적용됩니다)
apiKeystringBearer 인증을 활성화합니다. 요청에 Authorization: Bearer <key>를 담아 보냅니다
publicEndpointsInstaFixHttpMethod[]apiKey가 설정된 경우 ["POST", "OPTIONS"]인증을 건너뛰는 메서드입니다. 값을 지정하면 기본값을 완전히 대체합니다"POST"를 직접 포함시키지 않으면 위젯이 더 이상 제출할 수 없게 됩니다
requireAuthForDestructivebooleantrueapiKey가 없으면 PATCH와 DELETE는 401을 반환합니다. NODE_ENV=production에서는 apiKey 없이는 팩토리가 아예 시작을 거부합니다
redactUnauthenticatedEmailsbooleantrue응답에 노출되는 정보 참고
allowedOriginsstring[]완전 일치 방식의 CORS 허용 목록입니다. 와일드카드는 지원하지 않습니다["*"]는 아무것도 매칭하지 않으며, 설정하지 않으면 CORS 헤더가 전혀 붙지 않습니다
screenshotStorageScreenshotStorage스크린샷을 data URL로 인라인 처리하는 대신 실제 저장소에 업로드합니다. 커스텀 store를 전달하면 무시됩니다
caseInsensitiveSearchboolean자동 감지Prisma provider로부터 감지됩니다(PostgreSQL, MongoDB, CockroachDB에서는 켜짐). prisma를 지정했을 때만 적용됩니다
webhooksWebhookConfig | WebhookConfig[]새 피드백이 생성될 때마다 발동합니다 — 웹훅 참고

보안 모델, 있는 그대로

  • 읽기는 기본적으로 공개입니다. apiKey가 없으면 URL을 아는 누구나 피드백 목록을 조회할 수 있습니다(단, 이메일은 아래 설명대로 가려집니다).
  • 파괴적인 호출은 그렇지 않습니다. apiKey를 설정하거나 requireAuthForDestructive: false로 명시적으로 opt-out하지 않는 한, PATCH와 DELETE는 401을 반환합니다. 프로덕션에서는 키 없이 실행되는 대신 팩토리가 시작 시점에 예외를 던집니다.
  • OPTIONS는 항상 공개입니다 — 프리플라이트 요청은 반드시 동작해야 하기 때문입니다.

응답에 노출되는 정보

  • clientId모든 응답에서 제거됩니다.
  • authorEmail은 요청에 유효한 Authorization: Bearer 헤더가 없으면 비워집니다. 단, 예외가 하나 있습니다: POST 요청이 성공하면 작성자 본인에게는 이메일을 그대로 돌려줍니다.

HTTP 레퍼런스

엔드포인트는 하나, 메서드는 다섯 개입니다. 모든 바디는 JSON입니다.

메서드목적성공 시
POST피드백 생성(위젯 제출)201 + 레코드. clientId가 중복되면 실패하지 않고 기존 레코드를 반환합니다
GET피드백 목록 조회200 + { feedbacks, total }, Cache-Control: private, max-age=5 포함
PATCH상태 변경200 + 갱신된 레코드
DELETE하나 또는 전체 삭제200 + { deleted: true }
OPTIONSCORS 프리플라이트204

GET 쿼리 파라미터: projectName(필수), page(기본 1), limit(기본 50, 최대 100), type, status, statuses(콤마로 구분된 목록, 최대 4개 — 예: statuses=open,in_progress), search, url, urlPattern.

PATCH 바디: { id, projectName, status } — 세 필드 모두 필수입니다. statusopen, in_progress, resolved, wont_fix 중 하나입니다. resolvedAt은 서버가 자동으로 계산합니다: 상태가 닫힘(resolved/wont_fix)이면 값이 설정되고, 그 외에는 지워집니다.

DELETE 바디: 레코드 하나를 지울 때는 { id, projectName }, 프로젝트 전체를 지울 때는 { projectName, deleteAll: true }.

에러: 잘못된 JSON → 400 { error }; 유효성 검증 실패 → 400 { errors: [{ field, message }] }; 알 수 없는 id → 404; 테이블 없음 → 500과 함께 npx prisma db push 실행을 안내하는 힌트; 그 외 → 500 { error: "Internal server error" }.

알아 두어야 할 유효성 검증 한도

message는 5000자 이하 · annotations는 피드백당 50개 이하 · screenshotDataUrl은 1.5MB 이하, JPEG/PNG/WebP만 허용 · diagnostics는 콘솔 로그 50개, 네트워크 항목 20개까지 · clientId[a-zA-Z0-9_-]+ 패턴과 일치해야 함.

데이터베이스 스키마

이것이 어댑터가 필요로 하는 정확한 스키마이며, npx github:gnoopy/instafix#cli-dist sync가 생성하는 것과 동일합니다. 가능하면 CLI를 사용하세요. 직접 작성한다면 컬럼을 빠뜨리지 마세요: 어댑터는 모든 삽입 시 urlPattern, screenshotUrl, anchorKey를 함께 기록하므로, 스키마 일부가 빠지면 첫 제출부터 실패합니다.

model InstaFixFeedback {
  id               String               @id @default(cuid())
  projectName      String
  type             String
  message          String               @db.Text
  status           String               @default("open")
  url              String
  urlPattern       String?
  screenshotUrl    String?              @db.Text
  screenshotRegion Json?
  diagnostics      Json?
  viewport         String
  userAgent        String
  authorName       String
  authorEmail      String
  clientId         String               @unique
  resolvedAt       DateTime?
  createdAt        DateTime             @default(now())
  updatedAt        DateTime             @updatedAt
  annotations      InstaFixAnnotation[]

  @@index([projectName])
  @@index([projectName, status, createdAt])
  @@index([projectName, url])
}

model InstaFixAnnotation {
  id               String           @id @default(cuid())
  feedbackId       String
  feedback         InstaFixFeedback @relation(fields: [feedbackId], references: [id], onDelete: Cascade)
  cssSelector      String           @db.Text
  xpath            String           @db.Text
  textSnippet      String           @db.Text
  elementTag       String
  elementId        String?
  textPrefix       String           @db.Text
  textSuffix       String           @db.Text
  fingerprint      String
  neighborText     String           @db.Text
  anchorKey        String?
  xPct             Float
  yPct             Float
  wPct             Float
  hPct             Float
  scrollX          Float
  scrollY          Float
  viewportW        Int
  viewportH        Int
  devicePixelRatio Float            @default(1)
  createdAt        DateTime         @default(now())

  @@index([feedbackId])
}

@db.Text는 PostgreSQL과 MySQL을 대상으로 합니다. SQLite에서는 이 속성들을 제거하세요 — 거기서는 일반 String이 이미 크기 제한이 없습니다.

스크린샷 저장

기본적으로 스크린샷은 screenshotUrl 컬럼에 data: URL 형태로 인라인 저장됩니다 — 가볍게 시도해 보기에는 괜찮지만, 실제 데이터베이스에는 부담이 됩니다. 프로덕션에서는 ScreenshotStorage를 연결하세요:

createInstaFixHandler({
  prisma,
  screenshotStorage: {
    async upload(dataUrl, { feedbackId, mimeType }) {
      const url = await uploadToS3(dataUrl, `instafix/${feedbackId}.jpg`, mimeType);
      return { url };
    },
  },
});

업로드가 실패해도 피드백 자체는 저장되며(screenshotUrl: null 상태로), 경고 로그가 남습니다 — 스토리지 버킷이 고장 나도 고객이 남긴 코멘트를 잃어버리지 않습니다.

웹훅

새 피드백이 생길 때마다 알림을 받으세요:

createInstaFixHandler({
  prisma,
  webhooks: [
    { url: process.env.SLACK_WEBHOOK_URL!, type: "slack" },
    { url: "https://my-api.dev/hooks/instafix", type: "generic", headers: { "x-secret": "…" } },
  ],
});

type"slack", "discord", 또는 "generic"(기본값 — 가공되지 않은 원본 레코드를 그대로 전송하므로, generic 웹훅 대상은 신뢰할 수 있는 곳으로만 지정하세요)입니다. 각 호출에는 5초의 타임아웃이 적용되고, 실패해도 제출 자체는 절대 막히지 않으며, 선택적으로 onError(err, feedbackId)를 지정해 실패를 기록할 수 있습니다.

커스텀 스토어 사용하기

createInstaFixHandler({ store })는 어떤 InstaFixStore든(memory 어댑터든, 여러분이 직접 만든 것이든) 동일한 HTTP 인터페이스 뒤에 마운트합니다 — 검증, 인증, 정보 은닉 방식도 동일합니다.

소스 코드에서 확인한 주의사항이 두 가지 있습니다:

  • PATCH/DELETE에서의 프로젝트 간 소유권 검사는 선택적 메서드 store.verifyProjectOwnership을 덕타이핑으로 감지해서 동작합니다 — 구현만 하면 핸들러가 자동으로 적용합니다(PrismaStore, MemoryStore, LocalStorageStore 모두 구현되어 있습니다). 구현하지 않으면 피드백 id를 아는 누구든 projectName과 무관하게 수정할 수 있습니다 — 이런 커스텀 스토어 엔드포인트는 반드시 apiKey 뒤에 두세요.
  • screenshotStoragecaseInsensitiveSearch는 내장된 Prisma 경로에만 적용됩니다. 커스텀 스토어를 사용한다면 스크린샷 처리는 스토어 안에서 직접 구현해야 합니다.
GitHub에서 수정

이 페이지의 목차