f512d94c74
git-subtree-dir: clients/ds-app git-subtree-mainline:a24e3e6f22git-subtree-split:5206cf3b0c
202 lines
8.2 KiB
Swift
202 lines
8.2 KiB
Swift
import Foundation
|
|
|
|
/// Document list row. Non-optional set is intentional (B-2 review): only structural/envelope-grade
|
|
/// fields whose absence is a contract violation are non-optional, so a malformed response throws
|
|
/// loudly rather than silently nil-ing. `file_format` and `read_count` are OPTIONAL + fallback
|
|
/// (a note has no format; a light projection may omit the counter — one production null must not
|
|
/// kill the whole list decode). Everything else is optional (all-optional discipline).
|
|
public struct DocumentResponse: Codable, Sendable, Identifiable, Equatable {
|
|
// Invariants (loud-throw on absence)
|
|
public let id: Int
|
|
public let fileType: String
|
|
public let createdAtRaw: String
|
|
public let updatedAtRaw: String
|
|
// Optional + fallback
|
|
public let fileFormat: String?
|
|
public let readCount: Int?
|
|
// Everything else optional
|
|
public let filePath: String?
|
|
public let fileSize: Int?
|
|
public let title: String?
|
|
public let aiDomain: String?
|
|
public let aiSubGroup: String?
|
|
public let aiTags: [String]?
|
|
public let aiSummary: String?
|
|
public let documentType: String?
|
|
public let importance: String?
|
|
public let aiConfidence: Double?
|
|
public let userNote: String?
|
|
public let userTags: [String]?
|
|
public let pinned: Bool?
|
|
public let askIncludable: Bool?
|
|
public let derivedPath: String?
|
|
public let originalFormat: String?
|
|
public let conversionStatus: String?
|
|
public let isRead: Bool?
|
|
public let reviewStatus: String?
|
|
public let editUrl: String?
|
|
public let previewStatus: String?
|
|
public let sourceChannel: String?
|
|
public let dataOrigin: String?
|
|
public let docPurpose: String?
|
|
public let facetCompany: String?
|
|
public let facetTopic: String?
|
|
public let facetYear: Int?
|
|
public let facetDoctype: String?
|
|
public let category: String?
|
|
public let aiSuggestion: JSONValue?
|
|
public let aiTldr: String?
|
|
public let aiBullets: [String]?
|
|
public let aiDetailSummary: String?
|
|
public let aiInconsistencies: [String]?
|
|
public let aiAnalysisTier: String?
|
|
public let extractedAtRaw: String?
|
|
public let aiProcessedAtRaw: String?
|
|
public let embeddedAtRaw: String?
|
|
public let lastReadAtRaw: String?
|
|
// [S1-ADD]
|
|
public let originalFilename: String?
|
|
public let duplicateOf: Int?
|
|
public let duplicateCount: Int?
|
|
|
|
public var createdAt: Date? { DSDate.parse(createdAtRaw) }
|
|
public var updatedAt: Date? { DSDate.parse(updatedAtRaw) }
|
|
public var lastReadAt: Date? { DSDate.parse(lastReadAtRaw) }
|
|
public var reads: Int { readCount ?? 0 }
|
|
public var displayFormat: String {
|
|
fileFormat ?? filePath.map { ($0 as NSString).pathExtension }.flatMap { $0.isEmpty ? nil : $0 } ?? "?"
|
|
}
|
|
public var downloadLabel: String {
|
|
originalFilename ?? filePath.map { ($0 as NSString).lastPathComponent } ?? "원본"
|
|
}
|
|
public var hasDownloadableOriginal: Bool { fileType != "note" }
|
|
|
|
enum CodingKeys: String, CodingKey {
|
|
case id, title, importance, pinned, category
|
|
case fileType = "file_type"
|
|
case createdAtRaw = "created_at"
|
|
case updatedAtRaw = "updated_at"
|
|
case fileFormat = "file_format"
|
|
case readCount = "read_count"
|
|
case filePath = "file_path"
|
|
case fileSize = "file_size"
|
|
case aiDomain = "ai_domain"
|
|
case aiSubGroup = "ai_sub_group"
|
|
case aiTags = "ai_tags"
|
|
case aiSummary = "ai_summary"
|
|
case documentType = "document_type"
|
|
case aiConfidence = "ai_confidence"
|
|
case userNote = "user_note"
|
|
case userTags = "user_tags"
|
|
case askIncludable = "ask_includable"
|
|
case derivedPath = "derived_path"
|
|
case originalFormat = "original_format"
|
|
case conversionStatus = "conversion_status"
|
|
case isRead = "is_read"
|
|
case reviewStatus = "review_status"
|
|
case editUrl = "edit_url"
|
|
case previewStatus = "preview_status"
|
|
case sourceChannel = "source_channel"
|
|
case dataOrigin = "data_origin"
|
|
case docPurpose = "doc_purpose"
|
|
case facetCompany = "facet_company"
|
|
case facetTopic = "facet_topic"
|
|
case facetYear = "facet_year"
|
|
case facetDoctype = "facet_doctype"
|
|
case aiSuggestion = "ai_suggestion"
|
|
case aiTldr = "ai_tldr"
|
|
case aiBullets = "ai_bullets"
|
|
case aiDetailSummary = "ai_detail_summary"
|
|
case aiInconsistencies = "ai_inconsistencies"
|
|
case aiAnalysisTier = "ai_analysis_tier"
|
|
case extractedAtRaw = "extracted_at"
|
|
case aiProcessedAtRaw = "ai_processed_at"
|
|
case embeddedAtRaw = "embedded_at"
|
|
case lastReadAtRaw = "last_read_at"
|
|
case originalFilename = "original_filename"
|
|
case duplicateOf = "duplicate_of"
|
|
case duplicateCount = "duplicate_count"
|
|
}
|
|
}
|
|
|
|
/// Single-document detail = the flat DocumentResponse fields + markdown/body fields, decoded from the
|
|
/// same flat JSON object via composition (avoids duplicating ~45 field declarations). The pending-md
|
|
/// fixture decodes through the SAME struct.
|
|
public struct DocumentDetailResponse: Decodable, Sendable, Identifiable {
|
|
public let base: DocumentResponse
|
|
public let extractedText: String?
|
|
public let mdContent: String?
|
|
public let mdFrontmatter: JSONValue?
|
|
public let mdStatus: String?
|
|
public let mdExtractionQuality: JSONValue?
|
|
public let mdExtractionError: String?
|
|
public let mdExtractionEngine: String?
|
|
public let mdExtractionEngineVersion: String?
|
|
public let mdGeneratedAtRaw: String?
|
|
|
|
public var id: Int { base.id }
|
|
public var mdGeneratedAt: Date? { DSDate.parse(mdGeneratedAtRaw) }
|
|
/// MD-first render rule: render md_content when status is completed/partial, else fall back.
|
|
public var mdIsRenderable: Bool { mdStatus == "completed" || mdStatus == "partial" }
|
|
|
|
enum CodingKeys: String, CodingKey {
|
|
case extractedText = "extracted_text"
|
|
case mdContent = "md_content"
|
|
case mdFrontmatter = "md_frontmatter"
|
|
case mdStatus = "md_status"
|
|
case mdExtractionQuality = "md_extraction_quality"
|
|
case mdExtractionError = "md_extraction_error"
|
|
case mdExtractionEngine = "md_extraction_engine"
|
|
case mdExtractionEngineVersion = "md_extraction_engine_version"
|
|
case mdGeneratedAtRaw = "md_generated_at"
|
|
}
|
|
|
|
public init(from decoder: Decoder) throws {
|
|
self.base = try DocumentResponse(from: decoder)
|
|
let c = try decoder.container(keyedBy: CodingKeys.self)
|
|
self.extractedText = try c.decodeIfPresent(String.self, forKey: .extractedText)
|
|
self.mdContent = try c.decodeIfPresent(String.self, forKey: .mdContent)
|
|
self.mdFrontmatter = try c.decodeIfPresent(JSONValue.self, forKey: .mdFrontmatter)
|
|
self.mdStatus = try c.decodeIfPresent(String.self, forKey: .mdStatus)
|
|
self.mdExtractionQuality = try c.decodeIfPresent(JSONValue.self, forKey: .mdExtractionQuality)
|
|
self.mdExtractionError = try c.decodeIfPresent(String.self, forKey: .mdExtractionError)
|
|
self.mdExtractionEngine = try c.decodeIfPresent(String.self, forKey: .mdExtractionEngine)
|
|
self.mdExtractionEngineVersion = try c.decodeIfPresent(String.self, forKey: .mdExtractionEngineVersion)
|
|
self.mdGeneratedAtRaw = try c.decodeIfPresent(String.self, forKey: .mdGeneratedAtRaw)
|
|
}
|
|
}
|
|
|
|
public struct DocumentListResponse: Codable, Sendable {
|
|
public let items: [DocumentResponse]
|
|
public let total: Int
|
|
public let page: Int
|
|
public let pageSize: Int
|
|
|
|
enum CodingKeys: String, CodingKey {
|
|
case items, total, page
|
|
case pageSize = "page_size"
|
|
}
|
|
}
|
|
|
|
public struct DocumentContentResponse: Codable, Sendable {
|
|
public let id: Int
|
|
public let title: String?
|
|
public let domain: String?
|
|
public let subGroup: String?
|
|
public let documentType: String?
|
|
public let aiSummary: String?
|
|
public let aiTags: [String]?
|
|
public let content: String?
|
|
public let contentLength: Int?
|
|
public let truncated: Bool?
|
|
|
|
enum CodingKeys: String, CodingKey {
|
|
case id, title, domain, content, truncated
|
|
case subGroup = "sub_group"
|
|
case documentType = "document_type"
|
|
case aiSummary = "ai_summary"
|
|
case aiTags = "ai_tags"
|
|
case contentLength = "content_length"
|
|
}
|
|
}
|