Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
17 changes: 13 additions & 4 deletions app/api/companies/[ws]/files/route.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,19 @@ export async function GET(req, { params }) {
const denied = await guardCompany(ws); if (denied) return denied;
const rel = new URL(req.url).searchParams.get('rel') ?? '';
// Windows normalize()는 백슬래시를 반환 — 슬래시로 통일해야 files/ 접두 검사가 통과한다
const norm = normalize(rel).split('\\').join('/');
// _imported/ = 옵시디언 임포트 리포트·미분류 보관 — 경로만 만들어 두고 서빙 허용 목록에서 빠지면
// 사용자가 Finder로 찾아가야 한다(2026-07-20 신고와 같은 결함 계급 — 분리 검수 MED-5).
if (!(norm.startsWith('files/') || norm.startsWith('projects/') || norm.startsWith('_imported/')) || norm.includes('..')) {
let norm = normalize(rel).split('\\').join('/').replace(/^\/+/, '');
if (norm.includes('..')) {
return new Response('잘못된 경로', { status: 400 });
}
const vault = paths(ws).vault;
// projects/ 또는 files/ 접두사가 누락된 경우 projects/ 접두사 붙여서 존재 여부 시도
if (!(norm.startsWith('files/') || norm.startsWith('projects/') || norm.startsWith('_imported/'))) {
try {
await realpath(join(vault, `projects/${norm}`));
norm = `projects/${norm}`;
} catch {}
}
if (!(norm.startsWith('files/') || norm.startsWith('projects/') || norm.startsWith('_imported/'))) {
return new Response('잘못된 경로', { status: 400 });
}
try {
Expand Down
13 changes: 13 additions & 0 deletions app/c/[ws]/company-shell-context.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
'use client';

import { createContext, useContext } from 'react';

const CompanyShellContext = createContext(null);

export function CompanyShellProvider({ value, children }) {
return <CompanyShellContext.Provider value={value}>{children}</CompanyShellContext.Provider>;
}

export function useCompanyShell() {
return useContext(CompanyShellContext);
}
8 changes: 7 additions & 1 deletion app/c/[ws]/crew/[slug]/page.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -947,7 +947,7 @@ export default function CrewChat({ params }) {
) : m.who === 'user' ? (
<div key={i} className="msg-wrap fade-up" style={{ alignSelf: 'flex-end', alignItems: 'flex-end', maxWidth: '75%' }}
ref={(el) => { if (!m.mid) return; if (el) msgRefs.current.set(m.mid, el); else msgRefs.current.delete(m.mid); }}>
<div className="msg-user" style={{ alignSelf: 'auto', maxWidth: '100%', whiteSpace: 'pre-wrap', ...(m.failed ? { opacity: 0.72 } : {}) }}>
<div className="msg-user" style={{ alignSelf: 'auto', maxWidth: '100%', whiteSpace: 'pre-wrap', ...(m.failed ? { opacity: 0.72 } : {}), ...(m.pending ? { opacity: 0.7 } : {}) }}>
{m.attachments?.length > 0 && (
<span style={{ display: 'flex', flexWrap: 'wrap', gap: 6, marginBottom: m.text ? 8 : 0 }}>
{m.attachments.map((a, j) => a.isImage ? (
Expand All @@ -960,6 +960,12 @@ export default function CrewChat({ params }) {
</span>
)}
{m.text}
{m.pending && !viewing && (
<div style={{ display: 'flex', alignItems: 'center', gap: 6, marginTop: 6, fontSize: 12, color: 'var(--fg-3)' }}>
<ArgoSpinner size={12} />
<span>{t('chat.pending')}</span>
</div>
)}
</div>
{/* 실패한 턴 — 글은 스레드에 그대로 두고 사유와 재시도만 붙인다(호버로 숨지 않게 항상 표시) */}
{m.failed && !viewing && (
Expand Down
Loading