# 파일 업로드 없는 견적 요청 폼 만들기

> 자료를 업로드 대신 공유 링크로 받고, 모든 요청에 기한을 붙이고, 링크가 많은 리드가 스팸으로 분류되지 않게 하는 견적 요청 폼입니다.

2026-10-25 게시 · https://ikiform.com/ko/guides/lead-capture/quote-request

Ikiform은 파일 업로드를 `422 files_not_supported`로 거부하므로, 자료는 `url` 필드에 공유 폴더 링크로 받으세요. 모든 요청이 같은 형태로 도착하도록 서비스와 예산은 선택형으로 두고, 성공 메시지에서 처리 기간을 약속하세요. 그런 다음 각 요청을 기한과 함께 팀에 보내 약속이 지켜지게 하세요.

## 견적 요청이 다른 점

견적 요청에는 자료가 따라옵니다. 브리프, 평면도, 부품 스프레드시트, 작년 디자인 같은 것들입니다. 흔한 해답은 파일 입력이지만 여기서는 동작하지 않으며, 이를 피해 설계하기 전에 이유를 알아 둘 만합니다.

파일 업로드는 아직 지원하지 않습니다. 파일이 담긴 POST는 `422 files_not_supported`를 받습니다. 파일만이 아니라 POST 전체가 거부되므로, 폼이 오류를 보여 주고 입력을 유지하지 않으면 PDF를 첨부한 방문자는 입력한 모든 것을 잃습니다. 공유 링크는 이 문제를 피하고 나름의 장점도 있습니다. 고객이 보낸 뒤에도 파일을 추가할 수 있고, 크기 제한이 없고, 큰 파일이 받은편지함을 차지하지 않습니다.

다이어그램: 견적 요청 폼의 파일 입력과 링크 필드 비교

1. 파일 입력에서는 방문자가 brief.pdf를 첨부하고 브라우저가 multipart POST를 보냅니다.
2. Ikiform이 422 files_not_supported로 POST 전체를 거부합니다.
3. 아무것도 저장되지 않으므로 이메일도 웹훅도 나가지 않습니다.
4. 링크 필드에서는 방문자가 공유 폴더 링크를 붙여 넣습니다.
5. Ikiform이 유효한 https URL인지 확인하고 요청을 저장합니다.
6. 링크가 알림 이메일과 채팅에 도착하고, 팀의 누구나 열 수 있습니다.

같은 요청, 두 가지 결과. 폴더 링크를 요청하면 파일이 폼을 거칠 일이 없습니다.

## 필드

[견적 요청 템플릿](https://ikiform.com/forms/quote-request)에는 전화번호, 회사, 일정까지 모든 필드가 있습니다. 아래 버전은 이를 줄이고 링크 필드를 추가했습니다. `http:` 링크나 `drive.google.com`만 있는 링크를 넣어 `url` 필드가 어떻게 응답하는지 보세요.

폼: Quote request

- **이름** (`name`, text, 필수)
- **업무용 이메일** (`email`, email, 필수)
- **무엇이 필요하신가요?** (`service`, select, 필수)
- **예산** (`budget`, select)
- **파일 링크** (`files_url`, url)
- **견적에 무엇이 포함되어야 하나요?** (`details`, textarea, 필수)

`files_url`은 `https_only`인 `url` 필드이므로 `drive.google.com/...`만 넣으면 `https://drive.google.com/...`으로 저장되고, `http:` 링크는 거부됩니다.

| 필드 | 이런 형태인 이유 |
| --- | --- |
| `files_url` | 파일마다 필드 하나가 아니라 폴더 링크 하나. 안의 내용은 고객이 관리하고 나중에 추가할 수 있습니다 |
| `service` | 누가 견적을 낼지 정합니다. 레이블 이름을 바꿔도 값은 라우팅을 위해 유지됩니다 |
| `budget` | 선택이며, 아무것도 보내지 않는 **Not sure yet**(아직 모름) 선택지가 있습니다. 예산을 필수로 하면 리드를 잃습니다 |
| `details` | 필수, 최소 20자. 견적에는 범위가 필요하고, 여기서 나옵니다 |

## 폼

일반 HTML입니다. `type="url"`은 휴대폰에서 알맞은 키보드를 띄우고, 도움말이 폴더 공유 방법을 알려 주므로 첫 링크가 비공개였을 때 한 번 더 주고받을 일을 줄입니다.

`quote.html`:

```html
<form action="https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx" method="POST">
  <label for="name">Name</label>
  <input id="name" name="name" autocomplete="name" required />

  <label for="email">Work email</label>
  <input id="email" name="email" type="email" autocomplete="email" required />

  <label for="service">What do you need?</label>
  <select id="service" name="service" required>
    <option value="design">Design</option>
    <option value="development">Development</option>
    <option value="both">Design and development</option>
  </select>

  <label for="budget">Budget</label>
  <select id="budget" name="budget">
    <option value="">Not sure yet</option>
    <option value="under_5k">Under $5,000</option>
    <option value="5k_20k">$5,000 to $20,000</option>
    <option value="over_20k">Over $20,000</option>
  </select>

  <label for="files_url">Link to your files</label>
  <input id="files_url" name="files_url" type="url" inputmode="url" placeholder="https://drive.google.com/..." aria-describedby="files-help" />
  <p id="files-help">A shared folder with the brief, mockups or specs. Set it so anyone with the link can view.</p>

  <label for="details">What should the quote cover?</label>
  <textarea id="details" name="details" rows="5" minlength="20" required></textarea>

  <input type="hidden" name="_redirect" value="https://example.com/quote/thanks" />
  <input name="_gotcha" type="text" tabindex="-1" autocomplete="off" hidden />
  <button>Request a quote</button>
</form>
```

> 예전 폼에서 물려받은 `<input type="file">`이 있다면 제거하세요. 비어 있으면 건너뜁니다. 파일을 고르면 multipart나 `FormData` POST는 통째로 실패하고, 일반 urlencoded 폼은 파일 이름만 보냅니다. 어느 쪽이든 파일은 도착하지 않습니다.

## 링크와 스팸 점수

견적 폼은 실제 리드가 일부러 링크를 보내는 유일한 곳인데, 링크는 스팸 신호입니다. 모든 필드를 합쳐 링크가 3개 이상이면 점수에 2가 더해집니다. `url` 필드는 전체 `https://` 링크로 저장되므로 집계에 포함됩니다.

| 요청 내용 | 점수 | 결과 |
| --- | --- | --- |
| 폴더 링크 | 0 | 수신 |
| 폴더 링크와 `details`의 링크 두 개 | +2 | 수신 |
| 같은 링크 세 개를 같은 필드로 24시간 안에 다시 보냄 | +2와 +3 | 스팸, 5점 |

세 번째 행은 첫 제출이 실패했다고 생각한 실제 사람입니다. 그래서 폼에는 링크 필드가 세 개가 아니라 하나여야 하고, 성공 상태는 오해의 여지가 없어야 합니다. 매주 **Spam** 필터를 확인하세요. 스팸 아님으로 표시하면 그때 이메일과 웹훅이 전송됩니다.

## 모든 요청에 기한 붙이기

성공 메시지는 영업일 이틀 안에 견적을 약속합니다. 이 Cloudflare Worker는 그 약속을 팀이 볼 수 있는 날짜로 바꿉니다. 웹훅을 검증하고, `created_at`에서 기한을 계산하고, 파일 링크와 함께 요청을 견적 채널에 올립니다.

`src/index.ts`:

```ts
import { verifyWebhook, WebhookVerificationError } from "@ikiform/sdk/webhooks";

function addWorkingDays(from: Date, days: number) {
  const date = new Date(from);
  while (days > 0) {
    date.setUTCDate(date.getUTCDate() + 1);
    const day = date.getUTCDay();
    if (day !== 0 && day !== 6) days -= 1;
  }
  return date.toISOString().slice(0, 10);
}

export default {
  async fetch(request: Request, env: { IKIFORM_WEBHOOK_SECRET: string; SLACK_QUOTES_WEBHOOK: string }) {
    const payload = await request.text();
    let event;
    try {
      event = await verifyWebhook({ payload, headers: request.headers, secret: env.IKIFORM_WEBHOOK_SECRET });
    } catch (error) {
      if (error instanceof WebhookVerificationError) return new Response(error.reason, { status: 400 });
      throw error;
    }
    if (event.type !== "submission.created") return new Response(null, { status: 204 });

    const quote = event.data.fields;
    const due = addWorkingDays(new Date(event.data.created_at), 2);
    const lines = [
      `Quote due *${due}*: ${quote.service} for ${quote.name} (${quote.budget ?? "budget not given"})`,
      quote.files_url ? `Files: ${quote.files_url}` : "No files linked. Ask for them in the first reply.",
      `Reply: mailto:${quote.email}`,
    ];

    const response = await fetch(env.SLACK_QUOTES_WEBHOOK, {
      method: "POST",
      headers: { "content-type": "application/json" },
      body: JSON.stringify({ text: lines.join("\n") }),
    });
    if (!response.ok) return new Response(`Slack answered ${response.status}`, { status: 502 });
    return new Response(null, { status: 204 });
  },
};
```

- 영업일은 UTC 기준 토요일과 일요일을 건너뜁니다. 공휴일을 고려해 견적을 낸다면 추가하세요.
- `files_url`이 없으면 빈 줄 대신 첫 답장에서 파일을 요청하라는 알림을 만듭니다.
- Slack이 실패할 때 `502`로 응답하면 Ikiform이 같은 전달을 재시도하므로 요청이 누락되지 않습니다.

**Webhooks**에 Worker의 URL을 추가하고, `whsec_` 시크릿을 `IKIFORM_WEBHOOK_SECRET`에 복사하고, Slack URL을 시크릿으로 저장하세요. 수신 웹훅을 만드는 방법은 [Slack 가이드](https://ikiform.com/blog/form-submissions-to-slack.md)에서 다룹니다.

## 작동 확인하기

1. Google Drive나 Dropbox 폴더 링크로 제출하세요. 알림 이메일의 링크를 시크릿 창에서 열어 볼 수 있는지 확인하세요.
2. `http:` 링크로 제출하세요. POST가 `422`로 거부되고 메시지에 `files_url`이 지목됩니다.
3. 템플릿에서 `type="file"`을 검색해 Ikiform으로 POST하는 견적 폼에서 모두 제거하세요.
4. 견적 채널을 확인하세요. 메시지에 영업일 이틀 뒤의 기한과 파일 링크가 표시됩니다.
5. 알림 이메일에서 **Reply**를 누르세요. 폼에 입력한 주소로 작성됩니다.

## 주의할 점

- **비공개 링크.** 가장 흔한 실패는 소유자만 열 수 있는 링크입니다. 도움말에 공유 방법을 적고, 기다리지 말고 첫 답장에서 접근 권한을 요청하세요.
- **파일은 있는데 폴더가 없는 고객.** 첫 이메일에 첨부 파일로 답장하라고 안내하세요. reply-to 덕분에 파일이 팀과의 일반 이메일 스레드에 들어갑니다.
- **긴 범위 설명.** 알림 이메일은 값을 최대 500자까지 보여 줍니다. `details` 전체 텍스트는 대시보드와 웹훅에 있습니다.
- **접수 확인 이메일은 없습니다.** Ikiform은 폼을 작성한 사람에게 이메일을 보내지 않습니다. 페이지의 메시지가 접수 확인이므로 처리 기간을 구체적으로 적으세요.

## 질문

### 폼으로 파일 업로드를 받을 수 있나요?

아직은 안 됩니다. 파일이 포함된 POST는 422 files_not_supported로 거부됩니다. 대신 공유 드라이브 URL 같은 파일 링크를 요청하세요.

### 견적 폼으로 도면, 브리프, 사진을 어떻게 받나요?

url 필드에 공유 폴더 링크 하나를 요청하고, 도움말에 링크가 있는 누구나 볼 수 있어야 한다고 적으세요. 고객은 요청을 보낸 뒤에도 폴더에 파일을 추가할 수 있습니다.

### 링크가 여러 개인 견적 요청이 왜 스팸으로 분류됐나요?

링크가 3개 이상이면 스팸 점수에 2가 더해지고, 같은 필드를 24시간 안에 다시 보내면 3이 더해져 임계값 5에 닿습니다. 링크 필드를 하나로 유지하고, 요청을 스팸 아님으로 표시해 이메일과 웹훅을 보내세요.

### 예산 필드를 선택 입력으로 만들 수 있나요?

네. required를 끄고, 아직 모름처럼 값이 빈 첫 번째 옵션을 추가하세요. 빈 선택은 아무것도 보내지 않으며, Ikiform은 예산 없이 요청을 저장합니다.

## 가이드 더 보기

- [리드 수집 폼](https://ikiform.com/ko/guides/lead-capture.md): reply-to가 설정된 이메일을 보내고, 팀 채팅에 알리고, CRM에 넣는 내 사이트의 리드 수집 폼입니다. 리드가 아직 있을 때 답할 수 있습니다.
- [프로젝트 문의 폼](https://ikiform.com/ko/guides/lead-capture/project-inquiry.md): 누군가 브리프를 읽기 전에 예산과 시기로 문의를 유력, 관심, 부적합으로 분류하는 스튜디오와 프리랜서용 프로젝트 의뢰 폼입니다.
- [데모 요청 폼](https://ikiform.com/ko/guides/lead-capture/demo-request.md): 리드를 저장한 뒤 이름과 이메일이 미리 채워진 Cal.com이나 Calendly로 방문자를 보내는 데모 예약 폼입니다.
- [HubSpot이나 Pipedrive로 리드 보내기](https://ikiform.com/ko/guides/lead-capture/leads-to-crm.md): 서명된 Ikiform 웹훅을 HubSpot의 연락처 upsert나 Pipedrive의 person, lead 호출로 매핑하고, 전달이 재시도되어도 연락처가 중복되지 않게 합니다.
- [상담 신청 폼](https://ikiform.com/ko/guides/lead-capture/consultation-request.md): 회계사, 변호사, 코치, 클리닉을 위한 상담 신청 폼입니다. 연락 방법과 시간을 묻고, 민감한 내용은 통화에서 다룹니다.
- [Next.js 리드 폼](https://ikiform.com/ko/guides/lead-capture/nextjs.md): 서버 액션에서 Zod로 검증하고, 첫 유입 UTM을 쿠키에 보관하고, 자격을 갖춘 리드를 캘린더로 보내는 Next.js App Router 리드 폼입니다.
