소개
분실물 페이지에 이제 의미 있는 검색이 필요합니다. 파란 가방을 찾는 방문자에게 파란 물건 전부나 가방 전부를 반환해서는 안 됩니다. 없는 물품, 유효하지 않은 카테고리, 일치 항목이 없는 유효한 검색에도 서로 다른 응답이 필요합니다.
이 독립적인 작업 환경은 이전 가이드 랩의 목록 및 상세 핸들러에서 시작합니다. 해당 랩의 HTTP, JSON, httptest 개념 또는 동등한 경험이 필요합니다. 필터를 조합하고 명확한 오류를 도입하며 실제 페이지를 사용합니다. 읽기와 일반적인 실수를 포함해 약 55분을 예상하세요. 이는 작성자의 추정치입니다.
이름과 카테고리 필터 조합하기
이 단계에서는 두 검색 조건이 같은 레코드에 적용되도록 만듭니다. Workspace에서 시작하세요. 작업 디렉터리를 복원하고 준비된 Go 설치 경로를 이 터미널의 검색 경로에 추가합니다. export는 현재 셸에만 영향을 주며, $PATH는 기존 항목을 유지합니다.
cd /home/labex/project/lost-found
export PATH=/usr/local/go/bin:$PATH
먼저 동작하는 목록을 사용하고 데이터를 읽으세요.
curl -i 'http://127.0.0.1:8080/api/items'
cat items.json
여섯 레코드가 모두 예상됩니다. Lost and Found로 전환하여 blue를 입력하고 bags를 선택한 뒤 Search collection을 선택하세요. 현재 핸들러는 이 입력을 무시하므로 여전히 여섯 물품을 모두 보여 줍니다. 빠진 판단을 추가하도록 Workspace로 돌아갑니다.
URL의 쿼리 문자열은 ? 뒤에 옵니다. category=bags&q=blue는 &로 구분된 두 이름 있는 매개변수를 전달합니다. API 목록 검색은 category와 q만 각각 최대 한 번 허용합니다. 이 단계에서는 먼저 유효한 검색을 구현하고, 다음 단계에서 유효하지 않은 입력을 처리합니다.
계약은 정확합니다. 카테고리가 없거나 비어 있으면 모든 카테고리를 뜻합니다. 그렇지 않으면 철자와 대소문자가 items.json에 있는 카테고리와 일치해야 합니다. 키워드 q는 대소문자를 구분하지 않고 이름의 부분 문자열과 일치하며, 앞뒤 공백은 무시합니다. 키워드가 없거나 비어 있거나 공백뿐이면 모든 이름에 일치합니다. 두 필터를 모두 제공하면 둘 다 일치해야 합니다(AND). 일치하는 각 ID의 모든 필드를 중복 없이 유지하세요. 결과 순서는 자유롭습니다. 일치 항목이 없는 유효한 검색은 HTTP 200과 []를 반환합니다.
r.URL.Query().Get은 디코딩된 매개변수를 읽습니다. strings.TrimSpace는 앞뒤 공백을 제거하고, strings.ToLower는 Unicode 소문자 변환을 적용하며, strings.Contains는 부분 문자열을 검사합니다. 이는 단순한 소문자 비교이며 악센트 제거 또는 언어를 고려하는 검색 엔진이 아닙니다. make([]Item, 0)은 비어 있고 nil이 아닌 슬라이스를 만들어 JSON이 일치 항목 없음을 []로 인코딩하게 합니다. &&는 필요한 두 조건을 연결합니다. 여기서 ||를 쓰면 한 조건만 만족하는 레코드도 잘못 포함합니다.
편집 전에 items.json에서 결과 세 가지를 예측하세요. 키워드 blue만 쓸 때, 카테고리 bags만 쓸 때, 둘을 함께 쓸 때입니다. 교집합에는 LF-106만 속합니다. 두 우산은 어느 한 조건의 일치 항목만 반환하면 왜 틀리는지 보여 줍니다.
물려받은 상세 코드는 유지하세요. 첫 랩의 nano 절차를 사용하세요. Ctrl+W는 텍스트를 찾고 Ctrl+O와 Enter는 기존 파일 이름에 저장하며 Ctrl+X는 종료합니다. Go가 두 번째 핸들러를 컴파일하지 않도록 백업은 .go가 아닌 확장자를 사용해야 합니다.
cp handler.go handler.go.backup
LC_ALL=C.UTF-8 nano handler.go
/api/items 모음 분기를 찾아 모음 선택 로직만 바꾸세요. 응답 헤더, 상세 반복문, 없는 ID의 응답은 그대로 두세요. items 인수를 사용하여 아래 판단을 구현하세요.
r.URL.Query().Get으로 디코딩된category와q매개변수를 읽으세요. 카테고리는 주어진 그대로 유지하세요. 반복문 전에 키워드에strings.TrimSpace, 이어서strings.ToLower를 적용하세요.make([]Item, 0)으로 결과 슬라이스를 시작하세요. 원래 전체 슬라이스를 결과로 재사용하지 마세요. 그러면 일치하지 않는 레코드가 남습니다.for _, item := range items로 레코드를 순회하세요. 카테고리가 없거나 비었는지 또는item.Category와 같은지 판단하고, 별도로 소문자 이름이 정규화된 키워드를 포함하는지 판단하세요. 두 판단은&&로 연결하세요. 이름만 넣거나 다른 레코드의 ID를 가져오지 말고append로 일치하는item전체를 추가하세요.- 모음 분기에서 그 결과 슬라이스를 인코딩하고 반환하세요. 상세는 계속 ID로 선택하고 상세 요청에는 모음 필터를 적용하지 마세요.
비교를 위해 카테고리 조건은 category == "" || item.Category == category로 표현할 수 있습니다. 여기서 ||는 제한이 없거나 요청한 카테고리라는 뜻입니다. 카테고리와 이름 조건 사이의 AND를 대체하지 않습니다. 두 판단이 읽기 쉽도록 지역 변수 이름을 고르세요.
저장하고 종료한 뒤 실제로 저장한 파일을 살펴보세요.
cat handler.go
포맷하고 다시 빌드합니다. -w를 사용하면 gofmt가 파일을 갱신합니다. serve.py는 코드를 빌드하고 빌드가 성공한 뒤에만 기록된 작업 환경 서버를 교체합니다. 컴파일 오류가 있으면 이전 버전이 계속 실행됩니다. 보고된 줄을 수정하고 두 명령을 다시 실행하세요.
gofmt -w handler.go
python3 serve.py
이제 조합 검색을 보내세요. 따옴표는 &를 셸 연산자로 처리하지 않고 URL 안에 유지합니다.
curl -i 'http://127.0.0.1:8080/api/items?category=bags&q=blue'
Sports hall의 LF-106, Blue sports bag만 예상됩니다. 우산도 반환한다면 OR를 사용했다는 뜻입니다. 유효하지만 일치하지 않는 키워드는 빈 배열을 만들어야 합니다.
curl -i 'http://127.0.0.1:8080/api/items?q=not-in-the-collection'
공백, +, 비ASCII 문자를 포함한 텍스트는 curl이 인코딩하도록 하세요. -G는 제공한 데이터를 GET 쿼리 문자열로 보내고, --data-urlencode는 그 매개변수를 퍼센트 인코딩합니다. 인코딩하지 않으면 쿼리 데이터의 리터럴 더하기 기호가 공백으로 디코딩될 수 있습니다.
curl -G -i --data-urlencode 'q= CAFÉ + ' 'http://127.0.0.1:8080/api/items'
Welcome booth의 LF-104, Café + Map이 예상됩니다. 대문자 키워드와 앞뒤 공백은 정규화되지만 리터럴 더하기 기호는 이름의 일부로 남습니다. 경로를 잘못 입력했다면 cd /home/labex/project/lost-found로 복원하세요. 서비스가 이용 불가하면 cat으로 .work/server.log를 읽고 오류를 수정한 뒤 다시 빌드합니다.
유효하지 않은 요청과 빈 검색 결과 구분하기
이 단계에서는 상태와 JSON 모두로 오류를 확인할 수 있게 한 뒤 완성된 페이지를 사용합니다. 같은 디렉터리의 Workspace에서 시작하세요.
모음 쿼리에 알 수 없는 카테고리, 알 수 없는 매개변수, 반복된 매개변수가 포함되면 400을 사용하세요. 카테고리 철자는 정확해야 합니다. Bags와 bags는 다릅니다. 빈 category와 q는 계속 유효합니다. 알 수 없는 물품 ID나 알 수 없는 API 경로에는 404를 사용합니다. /api/items와 /api/items/<id>에서 GET이 아닌 메서드에는 Allow: GET을 포함한 405를 사용합니다. 오류 본문은 비어 있지 않은 error 문자열을 담은 객체이며 문구는 자유롭습니다. 일치 항목이 없는 유효한 검색은 200과 []를 유지합니다. 상세 요청은 ID로 선택하며 모음 쿼리 필터는 상세에 적용하지 않습니다. 준비된 홈페이지 /는 계속 이용 가능해야 합니다.
url.ParseQuery(r.URL.RawQuery)는 쿼리를 파싱하고 값과 오류를 모두 반환하므로 잘못된 쿼리 인코딩을 거부할 수 있습니다. 각 키는 값의 슬라이스에 대응하며, 그 길이로 반복된 매개변수를 알 수 있습니다. 이 예제는 고정 카테고리 목록이 아닌 현재 원본 레코드로 카테고리를 확인하므로 다른 안내소에서도 핸들러를 재사용할 수 있습니다.
본문을 쓰기 전에 헤더를 설정하세요. 조기 반환은 오류 처리에서 정상적인 성공 결과로 이어지는 일을 막습니다. 편집 전에 이전 단계의 유효하지만 빈 검색과 아래의 거부 요청을 비교하세요. 상태가 다른 이유를 예측하세요. 없는 물품 ID는 잘못된 모음 쿼리와 다른 실패입니다. 이전 단계의 검색 판단은 유지하세요. 이번 편집은 그 주변에 보호 조건을 추가합니다.
LC_ALL=C.UTF-8 nano handler.go
Ctrl+W로 반환되는 핸들러 함수를 찾으세요. url.ParseQuery를 위해 import 문에 net/url을 추가하세요. 파일 전체를 교체하지 말고 아래 보호 조건을 구현하세요.
먼저 경로부터 확인하세요. 모음 경로는 정확히 /api/items이며 상세는 /api/items/로 시작합니다. 다른 API 경로는 정상 페이지나 목록으로 이어지지 않고 404를 반환해야 합니다. 이어서 인식한 경로에서 메서드를 확인하세요. GET이 아니면 Allow를 GET으로 설정하고 405 JSON 오류를 반환하세요.
다음으로 필요하면 지역 오류 출력 함수 하나를 재사용하세요. 상태 정수와 메시지 문자열을 받아 w.WriteHeader(status)를 호출하고 map[string]string{"error": message}를 인코딩할 수 있습니다. content-type 헤더는 여전히 먼저 설정합니다. 비어 있지 않은 메시지를 고르세요. 문구는 채점하지 않습니다.
이어서 상세 선택을 모음 쿼리 파싱보다 앞에 두어 쿼리 필터가 ID 조회를 바꾸지 않게 하세요. 없는 상세는 여전히 JSON 오류와 함께 404를 반환합니다.
그다음 모음 분기에서 url.ParseQuery로 r.URL.RawQuery를 파싱하세요. 값을 사용하기 전에 반환된 오류를 확인하세요. 잘못된 인코딩은 400을 반환해야 합니다. 퍼센트 이스케이프에는 16진수 숫자 두 자리가 필요하므로 /api/items?q=%ZZ는 테스트에서 시도할 구체적인 잘못된 입력입니다. 각 키와 값 슬라이스를 순회하세요. category나 q 이외의 키와 길이가 1이 아닌 슬라이스는 거부하세요. 거부할 때마다 즉시 반환하세요.
다음으로 파싱한 값에서 허용된 매개변수를 읽으세요. 카테고리가 비어 있지 않으면 제공된 레코드를 순회해 그 정확한 카테고리가 존재하는지 판단하고, 없으면 400으로 거부하세요. items.json의 고정 카테고리 목록을 프로그램에 복사하지 마세요. 다른 안내소에는 완전히 새로운 카테고리가 있을 수 있습니다.
마지막으로 보호 조건을 모두 통과한 뒤 기존 AND 검색을 실행하고 결과 배열을 인코딩하세요. 빈 카테고리와 키워드 값은 계속 유효합니다. 일치하지 않는 결과는 오류로 바꾸지 말고 []를 유지하세요.
Ctrl+O와 Enter로 저장하고 Ctrl+X로 종료하세요. 편집기에 의도하지 않은 파일 이름이 표시되면 Ctrl+C로 프롬프트를 취소하고 경로를 확인하세요. 컴파일 오류를 고치는 동안 이미 동작하는 검색과 상세 코드를 보존하세요.
gofmt -w handler.go
python3 serve.py
다음은 의도적으로 거부되는 요청입니다. 차례로 400, 400, 404, 405가 예상됩니다. -X POST는 POST 메서드를 선택합니다. 읽기 전용 서비스이므로 아무것도 생성해서는 안 됩니다.
curl -i 'http://127.0.0.1:8080/api/items?category=unknown'
curl -i 'http://127.0.0.1:8080/api/items?q=blue&q=bag'
curl -i 'http://127.0.0.1:8080/api/not-a-route'
curl -X POST -i 'http://127.0.0.1:8080/api/items'
각 응답에는 JSON error 필드가 있고, POST에는 Allow: GET도 포함됩니다. curl이 이 응답을 받는 것은 HTTP 상태도 JSON 응답도 오지 않는 연결 실패와 다릅니다. 예상된 HTTP 오류를 작업 환경 초기화의 이유로 삼지 마세요.
테스트 케이스 테이블로 관련 기대값을 모으세요. 이전 랩의 레코더/요청 기법을 반복합니다. url.Values.Encode()는 Go에서 Unicode/더하기 기호 쿼리를 인코딩합니다. 목록 응답은 []Item으로 디코딩하고 내용을 살피기 전에 json.Unmarshal의 반환 오류를 확인하세요. 빈 결과에서는 nil이 아니고 길이가 0인 슬라이스가 []를 뜻하며 nil 슬라이스라면 null을 뜻합니다. 이미 실행한 실제 검색은 실제 물품 내용도 확인합니다.
실제 모음 대신 독립적인 레코드로 자신만의 테이블 기반 테스트를 만드세요. 두 카테고리에 같은 이름이 있고 세 번째는 일치하지 않는 작은 입력을 고르세요. 먼저 조합 검색의 정확한 ID 집합을 예측하세요. 상태만 검증하면 놓칠 OR 구현을 잡습니다.
LC_ALL=C.UTF-8 nano handler_test.go
첫 랩처럼 package main으로 시작하고 사용하는 패키지를 가져와 *testing.T를 받는 Test 함수에 테스트를 두세요. 요청/레코더에는 net/http/httptest, 디코딩에는 encoding/json, 검증에는 testing, 쿼리를 인코딩한다면 net/url을 사용하세요. 테스트 안에서 입력으로 핸들러를 한 번 만드세요.
익명 구조체의 슬라이스로 관련 요청을 표현하세요. 예를 들어 name string, path string, status int 필드가 각 케이스에 이름, 요청 경로, 예상 상태를 부여합니다. 직접 행을 작성하되 성공하는 조합 검색, 빈 검색, 유효하지 않은 카테고리, 반복 키, 잘못된 인코딩, 없는 ID를 포함하세요. for _, tc := range cases 반복문은 케이스마다 새 레코더에 요청을 적용합니다. httptest.NewRequest("GET", tc.path, nil)에 tc.path를 사용하고 recorder.Code를 tc.status와 비교하며 실패 출력에 케이스 이름을 포함하세요. 레코더를 재사용하면 응답 본문이 섞이므로 케이스마다 새로 만드세요.
Go의 t.Run(tc.name, func(t *testing.T) { ... })를 사용하면 선택적으로 각 행에 이름이 있는 하위 테스트를 만들 수 있습니다. 점 부분을 레코더, 요청, 검증 문장으로 바꾸세요. 실패한 t.Fatalf는 예상 상태와 관찰된 상태를 모두 알려야 합니다.
상태만으로는 검색을 증명할 수 없습니다. 성공하는 조합 케이스에서는 []Item으로 디코딩하고 디코딩 오류를 확인한 뒤 반환된 ID와 필드를 일치하는 테스트 입력 레코드와 비교하세요. 빈 케이스에서는 var got []Item을 선언하고 디코딩 오류를 확인한 뒤 got != nil과 len(got) == 0을 요구하세요. 유형을 바꾸지 않고 빈 배열과 null을 구분합니다. 거부 케이스에서는 map[string]string으로 디코딩하고 비어 있지 않은 error를 요구하세요. 인식된 경로에 별도 POST 요청을 추가하여 405와 recorder.Header().Get("Allow") == "GET"을 모두 확인하세요. 예상 결과가 다를 뿐 같은 레코더 기법을 사용합니다.
입력, 케이스 이름, 정확한 검증은 직접 정하세요. 같은 이름의 추가 물품이 결과에 포함되면 실패할 검증을 적어도 하나 만드세요. Ctrl+O와 Enter로 저장하고 Ctrl+X로 종료하세요.
gofmt -w handler_test.go
go test -v .
PASS가 예상됩니다. 테스트가 실패하면 표시된 경로와 상태를 위 계약과 비교하세요. 핸들러를 수정하고 테스트를 다시 실행한 뒤 브라우저가 현재 코드를 사용하도록 python3 serve.py로 다시 빌드합니다. 지원 파일 main.go, go.mod, items.json, serve.py, web/는 변경하지 마세요. 원본 모음이나 웹페이지를 교체하지 않고 handler.go를 편집하거나 직접 만든 Go 소스와 테스트를 추가 또는 편집할 수 있습니다.
Lost and Found로 전환해 새로 고칩니다. 카테고리 bags에서 blue를 검색하세요. 참조 번호 LF-106인 카드 하나가 예상됩니다. View item을 열어 Sports hall을 읽고 Back to collection으로 닫습니다. not-in-the-collection을 검색하면 0 items found와 No matching items가 예상됩니다. 키워드를 지우고 All categories를 선택해 다시 검색하면 여섯 레코드가 모두 돌아와야 합니다. Tab 키로 필드와 버튼에 이동하세요. Enter는 폼을 제출하고 Escape는 열린 상세 대화상자를 닫습니다.

선택한 카테고리, 한 건을 나타내는 결과 수, 일치하는 참조 번호를 보세요. 페이지는 검색과 상세 조회마다 API를 사용합니다. 이용 불가 메시지는 실제 요청 문제를 나타내며, 내장된 모음으로 대체하지 않습니다.
요약
정규화한 이름 필터와 정확히 일치하는 카테고리 필터를 조합하고, 물품의 식별 관계를 보존하고, 일치 항목이 없는 유효한 검색에 []를 반환하고, 400, 404, 405 오류를 구분했습니다. 테이블 기반 테스트와 실제 웹페이지가 핸들러를 실행합니다. 챌린지에서는 같은 계약을 다른 안내소의 데이터에 적용합니다.



