theorose49 a904cbf9b9
All checks were successful
build-and-push / build (push) Successful in 33s
feat: 메일함·근무상태 기록·프로필 사진·자동 프로비저닝 + 인센티브 유저 노출 제한
- 알림(Notification) 모델/이벤트 발행(프로젝트 추가·휴가/초과근무 승인·인센티브 반영/지급·정산 확정) + 메일함 API
- 근무상태 기록(WorkStatusEvent: 출근/퇴근/휴식/미팅/이동), 출퇴근은 Attendance도 갱신
- 남은 연차(소수점) 엔드포인트, 관리자 근무관리용 집계/로그 조회
- 프로필 사진(Member.AvatarKey) 업로드/스트리밍
- Keycloak 최초 로그인 자동 Member 프로비저닝(ensureMember, rank/부서 nullable)
- 프로젝트 scope=mine(나의 업무는 관리자도 본인 참여분만), nav에 메일함·근무관리·프로젝트관리·내프로필 추가
- 운영 안전: SEED 기본값 false(로컬만 SEED=true), ADMIN_GROUPS 기본 'admin'

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 09:38:33 +09:00

66 lines
2.7 KiB
Go

package httpapi
import (
"net/http"
"strings"
"spin/internal/models"
)
// MeResponse enriches the proxy identity with the matched Member profile.
type MeResponse struct {
User User `json:"user"`
Member *models.Member `json:"member"`
IsAdmin bool `json:"isAdmin"`
}
func (s *Server) handleMe(w http.ResponseWriter, r *http.Request) {
u := currentUser(r.Context())
writeJSON(w, http.StatusOK, MeResponse{
User: u,
Member: s.lookupMember(u.Email),
IsAdmin: s.isAdmin(r),
})
}
// NavItem is one sidebar entry; adminOnly entries are filtered for members.
type NavItem struct {
Key string `json:"key"`
Label string `json:"label"`
Path string `json:"path"`
Icon string `json:"icon"`
AdminOnly bool `json:"adminOnly"`
Section string `json:"section"`
}
var navItems = []NavItem{
{Key: "dashboard", Label: "대시보드", Path: "/", Icon: "LayoutDashboard", Section: "개요"},
{Key: "inbox", Label: "메일함", Path: "/inbox", Icon: "Inbox", Section: "개요"},
{Key: "attendance", Label: "근무", Path: "/attendance", Icon: "Clock", Section: "나의 업무"},
{Key: "projects", Label: "프로젝트", Path: "/projects", Icon: "FolderKanban", Section: "나의 업무"},
{Key: "incentive", Label: "인센티브", Path: "/incentive", Icon: "Coins", Section: "나의 업무"},
{Key: "profile", Label: "내 프로필", Path: "/profile", Icon: "UserCircle", Section: "나의 업무"},
{Key: "approvals", Label: "승인 관리", Path: "/admin/approvals", Icon: "CheckSquare", AdminOnly: true, Section: "관리자"},
{Key: "attendance-admin", Label: "근무 관리", Path: "/admin/attendance", Icon: "ClipboardList", AdminOnly: true, Section: "관리자"},
{Key: "projects-admin", Label: "프로젝트 관리", Path: "/admin/projects", Icon: "FolderCog", AdminOnly: true, Section: "관리자"},
{Key: "incentive-admin", Label: "인센티브 관리", Path: "/admin/incentive", Icon: "Calculator", AdminOnly: true, Section: "관리자"},
{Key: "accounting", Label: "회계", Path: "/admin/accounting", Icon: "Wallet", AdminOnly: true, Section: "관리자"},
{Key: "members", Label: "구성원", Path: "/admin/members", Icon: "Users", AdminOnly: true, Section: "관리자"},
{Key: "settings", Label: "설정", Path: "/admin/settings", Icon: "Settings", AdminOnly: true, Section: "관리자"},
}
func (s *Server) handleNav(w http.ResponseWriter, r *http.Request) {
admin := s.isAdmin(r)
out := make([]NavItem, 0, len(navItems))
for _, it := range navItems {
if it.AdminOnly && !admin {
continue
}
out = append(out, it)
}
writeJSON(w, http.StatusOK, out)
}
// lc lower-cases & trims a string (small helper used across handlers).
func lc(s string) string { return strings.ToLower(strings.TrimSpace(s)) }