Skip to content
Merged
Show file tree
Hide file tree
Changes from 41 commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
82279fa
refactor: introduce LlmClient with Letsur and OpenRouter providers
no-ikjun Jul 29, 2026
0362c62
refactor: split chat orchestration into focused services
no-ikjun Jul 29, 2026
3a7dad5
test: cover LLM providers and split chat orchestration services
no-ikjun Jul 29, 2026
87e7503
Merge pull request #44 from gsainfoteam/ikjun1101/cha3-6
no-ikjun Jul 29, 2026
df2287e
feat(db): add documents and chunks tables for PDF ingestion
no-ikjun Jul 30, 2026
136d56e
feat(config): add GCS and PDF processor environment settings
no-ikjun Jul 30, 2026
5883935
feat(llm): allow per-call timeout for long processor requests
no-ikjun Jul 30, 2026
cad944b
feat(prompts): add PDF page conversion and semantic chunking prompts
no-ikjun Jul 30, 2026
2066c6d
feat(pdf-processor): ingest PDFs asynchronously with GCS and DB worker
no-ikjun Jul 30, 2026
67f6733
feat(upload): route admin PDF uploads through internal processor
no-ikjun Jul 30, 2026
68f26fa
feat(retrieval): add DB-backed document catalog and content lookup
no-ikjun Jul 30, 2026
927c2d5
refactor(chat): use database retrieval for document selection
no-ikjun Jul 30, 2026
62c2322
refactor(pdf-processor): stop updating legacy resource index
no-ikjun Jul 30, 2026
4e97826
fix(pdf-processor): rely on inferred nullable document types
no-ikjun Jul 30, 2026
19a0b72
fix(env.validation): update GCS bucket and GCP project ID for testing…
no-ikjun Jul 30, 2026
a36597b
feat(pdf-processor): enhance chunk parsing with relative path normali…
no-ikjun Jul 30, 2026
d803286
feat(upload): update API responses to include DocumentListItemDto and…
no-ikjun Jul 30, 2026
2d6abf6
feat(upload): limit document reprocessing with cooldown
no-ikjun Jul 30, 2026
9d492c0
feat(documents): add document expiration support
no-ikjun Jul 30, 2026
6cb7189
fix(pdf-processor): increase chunking output limit
no-ikjun Jul 30, 2026
a0f476d
refactor(chat): update chunk selection logic and improve response for…
no-ikjun Jul 30, 2026
2df8ab0
refactor(pdf-processor): streamline PDF processing with metadata labe…
no-ikjun Jul 31, 2026
e8b9645
feat(pdf-processor): add Pass 1 page LLM fallback ratio configuration…
no-ikjun Jul 31, 2026
1391d76
refactor(pdf-processor): replace number parsing with parseFiniteNumbe…
no-ikjun Jul 31, 2026
ec30822
refactor(pdf-processor): improve error handling and cleanup in PDF te…
no-ikjun Jul 31, 2026
9029926
feat(upload): add 503 Service Unavailable response for GCS errors in …
no-ikjun Jul 31, 2026
66e2b35
refactor(retrieval): remove content field from ReadyDocumentWithChunk…
no-ikjun Jul 31, 2026
3aa6cd7
Merge pull request #45 from gsainfoteam/ikjun1101/cha3-7
no-ikjun Jul 31, 2026
1cba493
feat(docker): add OpenRouter configuration and Swagger API credential…
no-ikjun Jul 31, 2026
0533bc8
feat(chat): add Access-Control-Allow-Credentials header for CORS support
no-ikjun Jul 31, 2026
0bf3908
test(chat): add tests for SSE error handling and JSON parse failures …
no-ikjun Jul 31, 2026
c8413c5
feat(pdf-processor): implement normalizeRelativeChunkPath function an…
no-ikjun Jul 31, 2026
f019df9
feat(pdf-processor): enhance GcsStorageService to aggregate deletion …
no-ikjun Jul 31, 2026
038d523
feat(pdf-processor): implement heartbeat processing for document owne…
no-ikjun Jul 31, 2026
9ded3df
feat(journal): add new journal entry for aberrant_alex_wilder with br…
no-ikjun Jul 31, 2026
7e24bec
feat(retrieval): add support for preserving unknown dotted path segme…
no-ikjun Jul 31, 2026
af9ed51
feat(upload): enhance document management by adding ownership checks …
no-ikjun Jul 31, 2026
251166f
feat: add organization-based document management
BJDG-CM Aug 1, 2026
70b6e7f
docs: align upload detail Swagger response
BJDG-CM Aug 1, 2026
79024c1
fix: stabilize migration locking and document ordering
BJDG-CM Aug 1, 2026
40b7039
Merge pull request #47 from BJDG-CM/development
no-ikjun Aug 4, 2026
03a0b40
fix: improve migration locking and ensure client connection closure
no-ikjun Aug 4, 2026
10a373f
feat: add unique index for normalized email and improve admin identit…
no-ikjun Aug 7, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 25 additions & 5 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,28 @@ DOMAIN_NAME=example.com

# MCP Server Url
MCP_BASE_URL=your-mcp-server-url
MCP_RESOURCE_API_URL=your-mcp-resource-api-url

# Letsur AI Gateway
# GCS (PDF processor)
GCS_BUCKET=gcs-bucket-name
GCP_PROJECT_ID=gcp-project-id
# GCP 밖 배포 환경: 서비스 계정 JSON 전체를 한 줄 base64로 인코딩
# 값이 없으면 로컬 gcloud ADC 또는 GCP 런타임 서비스 계정을 사용
GCS_SERVICE_ACCOUNT_KEY_BASE64=

# PDF Processor
PDF_PROCESSOR_CONCURRENCY=1
PDF_PROCESSOR_CONTEXT_LENGTH=500
PDF_PROCESSOR_LLM_TIMEOUT=120
# Pass 1 page LLM fallback ratio (0–1). Exceeding this marks the document failed.
PDF_PROCESSOR_PASS1_MAX_FAILURE_RATIO=0.1
PDF_PROCESSOR_POLL_INTERVAL_MS=2000
# PDF_PROCESSOR_STALE_PROCESSING_MS=1800000

# LLM Provider: letsur (default) | openrouter
LLM_PROVIDER=letsur

# Letsur AI Gateway (LLM_PROVIDER=letsur)
LETSUR_AI_GATEWAY_BASE_URL=https://gw.letsur.ai/v1
LETSUR_AI_GATEWAY_API_KEY=your-letsur-ai-gateway-api-key
# LIGHT: 선별용(경로/chunk/문서 선택), NORMAL: 단순 응답, HEAVY: 최종 답변 생성
Expand All @@ -39,14 +59,14 @@ LETSUR_AI_GATEWAY_MODEL_NORMAL=gpt-4o-mini
LETSUR_AI_GATEWAY_MODEL_HEAVY=gpt-4o-mini
LETSUR_AI_GATEWAY_X_TITLE=GIST_CHATBOT

# Legacy: Open Router
# 기존 OpenRouter 연동용 환경변수입니다. Letsur AI Gateway 전환 후에는 기본적으로 사용하지 않습니다.
# OpenRouter (LLM_PROVIDER=openrouter 일 때만 필수)
OPEN_ROUTER_API_KEY=your-openrouter-api-key
OPEN_ROUTER_BASE_URL=https://openrouter.ai/api/v1
OPEN_ROUTER_MODEL_LIGHT=google/gemini-2.0-flash-001
OPEN_ROUTER_MODEL_NORMAL=google/gemini-2.0-flash-001
OPEN_ROUTER_MODEL_HEAVY=openai/gpt-4o
OPEN_ROUTER_X_TITLE=APP_NAME

# Swagger API 문서 잠금
# SWAGGER_USER=docs_admin
# SWAGGER_PASSWORD=your-secure-password
SWAGGER_USER=docs_admin
SWAGGER_PASSWORD=your-secure-password
154 changes: 146 additions & 8 deletions bun.lock

Large diffs are not rendered by default.

24 changes: 22 additions & 2 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@ services:
- '3000:3000'
environment:
# Database (Docker에서만 다른 값)
DB_HOST: postgres # Docker 네트워크 내 서비스 이름
NODE_ENV: production # Docker는 프로덕션 모드
DB_HOST: postgres # Docker 네트워크 내 서비스 이름
NODE_ENV: production # Docker는 프로덕션 모드
# 나머지는 .env 파일에서 가져옴
DB_PORT: ${DB_PORT:-5432}
DB_USER: ${DB_USER:-postgres}
Expand All @@ -50,6 +50,15 @@ services:
# MCP Configuration (optional)
MCP_BASE_URL: ${MCP_BASE_URL:-}
MCP_RESOURCE_API_URL: ${MCP_RESOURCE_API_URL:-}
# GCS / PDF Processor
GCS_BUCKET: ${GCS_BUCKET:-}
GCP_PROJECT_ID: ${GCP_PROJECT_ID:-}
GCS_SERVICE_ACCOUNT_KEY_BASE64: ${GCS_SERVICE_ACCOUNT_KEY_BASE64:-}
PDF_PROCESSOR_CONCURRENCY: ${PDF_PROCESSOR_CONCURRENCY:-1}
PDF_PROCESSOR_CONTEXT_LENGTH: ${PDF_PROCESSOR_CONTEXT_LENGTH:-500}
PDF_PROCESSOR_LLM_TIMEOUT: ${PDF_PROCESSOR_LLM_TIMEOUT:-120}
PDF_PROCESSOR_PASS1_MAX_FAILURE_RATIO: ${PDF_PROCESSOR_PASS1_MAX_FAILURE_RATIO:-0.1}
PDF_PROCESSOR_POLL_INTERVAL_MS: ${PDF_PROCESSOR_POLL_INTERVAL_MS:-2000}
# Letsur AI Gateway Configuration
LETSUR_AI_GATEWAY_BASE_URL: ${LETSUR_AI_GATEWAY_BASE_URL:-}
LETSUR_AI_GATEWAY_API_KEY: ${LETSUR_AI_GATEWAY_API_KEY:-}
Expand All @@ -58,6 +67,17 @@ services:
LETSUR_AI_GATEWAY_MODEL_NORMAL: ${LETSUR_AI_GATEWAY_MODEL_NORMAL:-}
LETSUR_AI_GATEWAY_MODEL_HEAVY: ${LETSUR_AI_GATEWAY_MODEL_HEAVY:-}
LETSUR_AI_GATEWAY_X_TITLE: ${LETSUR_AI_GATEWAY_X_TITLE:-}
LLM_PROVIDER: ${LLM_PROVIDER:-letsur}
Comment thread
no-ikjun marked this conversation as resolved.
# OpenRouter Configuration (LLM_PROVIDER=openrouter)
OPEN_ROUTER_API_KEY: ${OPEN_ROUTER_API_KEY:-}
OPEN_ROUTER_BASE_URL: ${OPEN_ROUTER_BASE_URL:-}
OPEN_ROUTER_MODEL_LIGHT: ${OPEN_ROUTER_MODEL_LIGHT:-}
OPEN_ROUTER_MODEL_NORMAL: ${OPEN_ROUTER_MODEL_NORMAL:-}
OPEN_ROUTER_MODEL_HEAVY: ${OPEN_ROUTER_MODEL_HEAVY:-}
OPEN_ROUTER_X_TITLE: ${OPEN_ROUTER_X_TITLE:-}
# Swagger API 문서 잠금
SWAGGER_USER: ${SWAGGER_USER:-}
SWAGGER_PASSWORD: ${SWAGGER_PASSWORD:-}
depends_on:
postgres:
condition: service_healthy
Expand Down
34 changes: 34 additions & 0 deletions drizzle/0009_goofy_starjammers.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
CREATE TYPE "public"."document_status" AS ENUM('queued', 'processing', 'ready', 'failed');--> statement-breakpoint
CREATE TABLE "document_chunks" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"document_id" uuid NOT NULL,
"path" varchar(1024) NOT NULL,
"description" text DEFAULT '' NOT NULL,
"content" text NOT NULL,
"sort_order" integer DEFAULT 0 NOT NULL,
"created_at" timestamp DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "documents" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"title" varchar(512) NOT NULL,
"resource_name" varchar(512) NOT NULL,
"summary" text,
"gcs_pdf_path" varchar(1024) NOT NULL,
"status" "document_status" DEFAULT 'queued' NOT NULL,
"error_message" text,
"uploaded_by_idp_uuid" varchar(255) NOT NULL,
"is_active" boolean DEFAULT true NOT NULL,
"created_at" timestamp DEFAULT now() NOT NULL,
"updated_at" timestamp DEFAULT now() NOT NULL,
"processed_at" timestamp
);
--> statement-breakpoint
ALTER TABLE "document_chunks" ADD CONSTRAINT "document_chunks_document_id_documents_id_fk" FOREIGN KEY ("document_id") REFERENCES "public"."documents"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
CREATE INDEX "document_chunks_document_id_idx" ON "document_chunks" USING btree ("document_id");--> statement-breakpoint
CREATE INDEX "document_chunks_document_sort_idx" ON "document_chunks" USING btree ("document_id","sort_order");--> statement-breakpoint
CREATE UNIQUE INDEX "documents_resource_name_active_unique" ON "documents" USING btree ("resource_name") WHERE "documents"."is_active" = true;--> statement-breakpoint
CREATE INDEX "documents_status_idx" ON "documents" USING btree ("status");--> statement-breakpoint
CREATE INDEX "documents_uploaded_by_idp_uuid_idx" ON "documents" USING btree ("uploaded_by_idp_uuid");--> statement-breakpoint
CREATE INDEX "documents_is_active_idx" ON "documents" USING btree ("is_active");--> statement-breakpoint
CREATE INDEX "documents_created_at_idx" ON "documents" USING btree ("created_at");
2 changes: 2 additions & 0 deletions drizzle/0010_gray_the_order.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
ALTER TYPE "public"."document_status" ADD VALUE 'uploading' BEFORE 'queued';--> statement-breakpoint
ALTER TABLE "documents" ADD COLUMN "processing_token" uuid;
1 change: 1 addition & 0 deletions drizzle/0011_odd_chimera.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ALTER TABLE "documents" ADD COLUMN "last_reprocessed_at" timestamp;
2 changes: 2 additions & 0 deletions drizzle/0012_brainy_dagger.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
ALTER TABLE "documents" ADD COLUMN "expires_at" timestamp;--> statement-breakpoint
CREATE INDEX "documents_expires_at_idx" ON "documents" USING btree ("expires_at");
2 changes: 2 additions & 0 deletions drizzle/0013_aberrant_alex_wilder.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
CREATE INDEX "document_chunks_path_idx" ON "document_chunks" USING btree ("path");--> statement-breakpoint
CREATE UNIQUE INDEX "document_chunks_document_id_path_unique" ON "document_chunks" USING btree ("document_id","path");

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# document_chunks 삽입 경로와 중복 처리 방식을 확인한다.
set -euo pipefail

rg -n --type=ts -C 6 'documentChunks' src | head -200

Repository: gsainfoteam/chatbot-be

Length of output: 12619


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== migration =="
cat -n drizzle/0013_aberrant_alex_wilder.sql

echo
echo "== schema relevant =="
sed -n '324,348p' src/db/schema.ts | cat -n

echo
echo "== document chunk insert/reprocess paths =="
sed -n '170,235p' src/pdf-processor/documents.repository.ts | cat -n

echo
echo "== organizations delete chunks path =="
sed -n '860,888p' src/organizations/organizations.repository.ts | cat -n

echo
echo "== all documentChunks insert/delete/upsert occurrences =="
rg -n --type=ts \
  "insert\\(documentChunks\\)|delete\\(documentChunks\\)|sql`|onConflict|onConflictDoUpdate|replaceChunks|setShare|setDocumentStatus" src

Repository: gsainfoteam/chatbot-be

Length of output: 4968


중복 document_id + path를 제거한 후 유니크 인덱스를 추가하세요.

현재 completeProcessing()은 문서별 청크를 먼저 삭제한 뒤 배치 삽입하므로 재처리 충돌보다는 기존 데이터의 중복 (document_id, path)CREATE UNIQUE INDEX 실패 원인이 됩니다. 마이그레이션 전에 중복이 없으면 배포해도 되고, 중복이 있을 수 있으면 인덱스 생성 전 정리 문장을 추가하세요.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@drizzle/0013_aberrant_alex_wilder.sql` at line 2, Update migration 0013 so
duplicate document_chunks rows sharing the same document_id and path are removed
before creating document_chunks_document_id_path_unique; retain one row per
pair, then create the unique index.

112 changes: 112 additions & 0 deletions drizzle/0014_unique_dracula.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
CREATE TYPE "public"."organization_membership_status" AS ENUM('PENDING', 'ACCEPTED');--> statement-breakpoint
CREATE TYPE "public"."organization_role" AS ENUM('MANAGER', 'MEMBER');--> statement-breakpoint
CREATE TABLE "document_organization_shares" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"document_id" uuid NOT NULL,
"organization_id" uuid NOT NULL,
"shared_by_idp_uuid" varchar(255) NOT NULL,
"created_at" timestamp DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "document_ownership_transfers" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"document_id" uuid NOT NULL,
"source_organization_id" uuid NOT NULL,
"target_organization_id" uuid NOT NULL,
"actor_idp_uuid" varchar(255) NOT NULL,
"transferred_at" timestamp DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "organization_memberships" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"organization_id" uuid NOT NULL,
"invitee_email" varchar(255) NOT NULL,
"member_idp_uuid" varchar(255),
"role" "organization_role" DEFAULT 'MEMBER' NOT NULL,
"status" "organization_membership_status" DEFAULT 'PENDING' NOT NULL,
"invited_by_idp_uuid" varchar(255) NOT NULL,
"accepted_at" timestamp,
"created_at" timestamp DEFAULT now() NOT NULL,
"updated_at" timestamp DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "organizations" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"name" varchar(255) NOT NULL,
"slug" varchar(255) NOT NULL,
"is_default" boolean DEFAULT false NOT NULL,
"created_by_idp_uuid" varchar(255),
"created_at" timestamp DEFAULT now() NOT NULL,
"updated_at" timestamp DEFAULT now() NOT NULL,
CONSTRAINT "organizations_slug_unique" UNIQUE("slug")
);
--> statement-breakpoint
INSERT INTO "organizations" ("name", "slug", "is_default", "created_by_idp_uuid")
VALUES ('인포팀', 'infoteam', true, NULL)
ON CONFLICT ("slug") DO UPDATE
SET "name" = EXCLUDED."name",
"is_default" = true,
"updated_at" = now();--> statement-breakpoint
INSERT INTO "organization_memberships" (
"organization_id",
"invitee_email",
"member_idp_uuid",
"role",
"status",
"invited_by_idp_uuid",
"accepted_at"
)
SELECT
o."id",
lower(trim(a."email")),
a."idp_uuid",
'MANAGER'::"organization_role",
'ACCEPTED'::"organization_membership_status",
a."idp_uuid",
now()
FROM "admins" a
JOIN "organizations" o ON o."slug" = 'infoteam'
WHERE a."role" = 'SUPER_ADMIN'
AND NOT EXISTS (
SELECT 1
FROM "organization_memberships" existing
WHERE existing."organization_id" = o."id"
AND (
existing."invitee_email" = lower(trim(a."email"))
OR existing."member_idp_uuid" = a."idp_uuid"
)
);--> statement-breakpoint
ALTER TABLE "documents" ADD COLUMN "owner_organization_id" uuid;--> statement-breakpoint
UPDATE "documents"
SET "owner_organization_id" = (
SELECT "id" FROM "organizations" WHERE "slug" = 'infoteam'
)
WHERE "owner_organization_id" IS NULL;--> statement-breakpoint
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM "documents" WHERE "owner_organization_id" IS NULL) THEN
RAISE EXCEPTION 'documents.owner_organization_id backfill failed';
END IF;
END $$;--> statement-breakpoint
ALTER TABLE "documents" ALTER COLUMN "owner_organization_id" SET NOT NULL;--> statement-breakpoint
ALTER TABLE "document_organization_shares" ADD CONSTRAINT "document_organization_shares_document_id_documents_id_fk" FOREIGN KEY ("document_id") REFERENCES "public"."documents"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "document_organization_shares" ADD CONSTRAINT "document_organization_shares_organization_id_organizations_id_fk" FOREIGN KEY ("organization_id") REFERENCES "public"."organizations"("id") ON DELETE restrict ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "document_ownership_transfers" ADD CONSTRAINT "document_ownership_transfers_document_id_documents_id_fk" FOREIGN KEY ("document_id") REFERENCES "public"."documents"("id") ON DELETE restrict ON UPDATE no action;--> statement-breakpoint

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# documents 테이블의 하드 삭제 경로를 확인한다.
set -euo pipefail

rg -n --type=ts -C 6 'delete\(documents\)|from\(documents\)\s*\)|\.delete\(' src | head -200

Repository: gsainfoteam/chatbot-be

Length of output: 13940


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== migration constraints around document_ownership_transfers =="
sed -n '1,140p' drizzle/0014_unique_dracula.sql | nl -ba

echo
echo "== upload/delete controller and service outlines =="
fd -a 'upload\.(controller|service)\.ts$' src | sed 's#^\./##' | while read -r f; do
  echo "--- $f"
  ast-grep outline "$f" --match delete --view compact || true
done

echo
echo "== relevant source snippets =="
sed -n '340,410p' src/upload/upload.controller.ts | nl -ba
sed -n '1,220p' src/upload/upload.service.ts | nl -ba

echo
echo "== repository hardDelete usages =="
rg -n -C 4 'hardDelete\(' src | sed -n '1,220p'

echo
echo "== all delete(documents) occurrences =="
rg -n -C 4 'delete\(documents\)' src drizzle | sed -n '1,220p'

Repository: gsainfoteam/chatbot-be

Length of output: 264


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== migration constraints around document_ownership_transfers =="
sed -n '1,140p' drizzle/0014_unique_dracula.sql

echo
echo "== upload/delete controller snippets =="
sed -n '340,410p' src/upload/upload.controller.ts

echo
echo "== upload service snippets =="
sed -n '1,220p' src/upload/upload.service.ts

echo
echo "== hardDelete usages =="
rg -n -C 4 'hardDelete\(' src

echo
echo "== delete(documents) occurrences =="
rg -n -C 4 'delete\(documents\)' src drizzle

Repository: gsainfoteam/chatbot-be

Length of output: 19218


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== rollbackUpload references =="
rg -n -C 6 'rollbackUpload|rollback' src/upload/upload.service.ts

echo
echo "== DocumentsRepository source around chunk ownership and soft delete =="
sed -n '1,260p' src/pdf-processor/documents.repository.ts

echo
echo "== OrganizationsRepository around cancelAndSoftDeleteDocument =="
sed -n '820,920p' src/organizations/organizations.repository.ts

echo
echo "== ownership transfer insert context =="
rg -n -C 8 'documentOwnershipTransfers|ownershipTransfers|ownership transfer|transfer' src | sed -n '1,260p'

Repository: gsainfoteam/chatbot-be

Length of output: 29483


document_ownership_transfers 삭제 정책을 보존으로 설정하세요.

이 마이그레이션은 document_organization_shares.document_idON DELETE cascade로 설정하지만, document_ownership_transfers.document_idON DELETE restrict로 남깁니다. 파일 삭제 시 DocumentsRepository.hardDelete()가 실행되므로, 해당 문서의 ownership transfer가 있으면 외래 키 위반으로 로그 롤백도 실패합니다. 감사 기록만 남길 수 없다면 cascade를 사용하세요.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@drizzle/0014_unique_dracula.sql` at line 94, document_ownership_transfers의
document_id 외래 키 제약 조건에서 삭제 정책을 ON DELETE restrict에서 ON DELETE cascade로 변경하세요.
document_organization_shares의 기존 cascade 정책과 일관되게 설정하고,
DocumentsRepository.hardDelete()가 문서와 관련 ownership transfer를 함께 삭제하도록 유지하세요.

ALTER TABLE "document_ownership_transfers" ADD CONSTRAINT "document_ownership_transfers_source_organization_id_organizations_id_fk" FOREIGN KEY ("source_organization_id") REFERENCES "public"."organizations"("id") ON DELETE restrict ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "document_ownership_transfers" ADD CONSTRAINT "document_ownership_transfers_target_organization_id_organizations_id_fk" FOREIGN KEY ("target_organization_id") REFERENCES "public"."organizations"("id") ON DELETE restrict ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "organization_memberships" ADD CONSTRAINT "organization_memberships_organization_id_organizations_id_fk" FOREIGN KEY ("organization_id") REFERENCES "public"."organizations"("id") ON DELETE restrict ON UPDATE no action;--> statement-breakpoint
CREATE INDEX "document_organization_shares_document_id_idx" ON "document_organization_shares" USING btree ("document_id");--> statement-breakpoint
CREATE INDEX "document_organization_shares_organization_id_idx" ON "document_organization_shares" USING btree ("organization_id");--> statement-breakpoint
CREATE UNIQUE INDEX "document_organization_shares_document_id_organization_id_unique" ON "document_organization_shares" USING btree ("document_id","organization_id");--> statement-breakpoint
CREATE INDEX "document_ownership_transfers_document_id_idx" ON "document_ownership_transfers" USING btree ("document_id");--> statement-breakpoint
CREATE INDEX "document_ownership_transfers_source_organization_id_idx" ON "document_ownership_transfers" USING btree ("source_organization_id");--> statement-breakpoint
CREATE INDEX "document_ownership_transfers_target_organization_id_idx" ON "document_ownership_transfers" USING btree ("target_organization_id");--> statement-breakpoint
CREATE INDEX "organization_memberships_organization_id_idx" ON "organization_memberships" USING btree ("organization_id");--> statement-breakpoint
CREATE INDEX "organization_memberships_member_idp_uuid_status_idx" ON "organization_memberships" USING btree ("member_idp_uuid","status");--> statement-breakpoint
CREATE INDEX "organization_memberships_invitee_email_status_idx" ON "organization_memberships" USING btree ("invitee_email","status");--> statement-breakpoint
CREATE UNIQUE INDEX "organization_memberships_organization_id_invitee_email_unique" ON "organization_memberships" USING btree ("organization_id","invitee_email") WHERE "organization_memberships"."status" = 'PENDING';--> statement-breakpoint
CREATE UNIQUE INDEX "organization_memberships_organization_id_member_idp_uuid_unique" ON "organization_memberships" USING btree ("organization_id","member_idp_uuid") WHERE "organization_memberships"."member_idp_uuid" IS NOT NULL;--> statement-breakpoint
CREATE UNIQUE INDEX "organizations_single_default_unique" ON "organizations" USING btree ("is_default") WHERE "organizations"."is_default" = true;--> statement-breakpoint
CREATE INDEX "organizations_created_by_idp_uuid_idx" ON "organizations" USING btree ("created_by_idp_uuid");--> statement-breakpoint
ALTER TABLE "documents" ADD CONSTRAINT "documents_owner_organization_id_organizations_id_fk" FOREIGN KEY ("owner_organization_id") REFERENCES "public"."organizations"("id") ON DELETE restrict ON UPDATE no action;--> statement-breakpoint
CREATE INDEX "documents_owner_organization_id_idx" ON "documents" USING btree ("owner_organization_id");
Loading