Skip to content
Merged
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
31 changes: 26 additions & 5 deletions backend/src/services/resultService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,23 @@ export const resultService = {
throw { status: 404, code: 'user_not_found', message: 'User not found' };
}

// ベースラインスコア組み立て(キャッシュ有無に関わらず必要)
const baseline_scores: BaselineScores = {
// 1. ベースラインスコア取得 (アンケート結果)
let baseline_scores: BaselineScores = {
caution: user.baseline_caution,
calmness: user.baseline_calmness,
logic: user.baseline_logic,
cooperativeness: user.baseline_coop,
positivity: user.baseline_positive,
};

// 2. MBTI理論値スコア取得
const mbti_scores = user.self_mbti ? getMbtiScores(user.self_mbti) : null;

// 3. MBTIが回答されている場合、ベースラインを理論値で補正する(極端な値を抑える)
if (mbti_scores) {
baseline_scores = this.blendScores(baseline_scores, mbti_scores);
}

// キャッシュ確認:analysis_results にあればそこから返す
const cached = await analysisResultRepository.findByUserId(userId);
if (cached) {
Expand All @@ -49,9 +57,6 @@ export const resultService = {
baseline_scores
);

// MBTI理論値スコア
const mbti_scores = user.self_mbti ? getMbtiScores(user.self_mbti) : null;

// analysis_results にキャッシュとして保存
const cacheRow: AnalysisResultRow = {
user_id: userId,
Expand Down Expand Up @@ -133,5 +138,21 @@ export const resultService = {
details: cached.details,
};
},

/**
* アンケート結果とMBTI理論値をブレンドして、ベースラインの極端な値を調整する
*/
blendScores(survey: BaselineScores, mbti: BaselineScores): BaselineScores {
// 比重: アンケート 70%, 理論値 30%
const blend = (s: number, m: number) => Math.round(s * 0.7 + m * 0.3);

return {
caution: blend(survey.caution, mbti.caution),
calmness: blend(survey.calmness, mbti.calmness),
logic: blend(survey.logic, mbti.logic),
cooperativeness: blend(survey.cooperativeness, mbti.cooperativeness),
positivity: blend(survey.positivity, mbti.positivity),
};
},
};

10 changes: 10 additions & 0 deletions frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"dependencies": {
"framer-motion": "^12.34.2",
"jotai": "^2.18.0",
"lucide-react": "^0.575.0",
"next": "16.1.6",
"react": "19.2.3",
"react-dom": "19.2.3",
Expand Down
31 changes: 25 additions & 6 deletions frontend/src/features/result/components/FeatureScoreBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,20 @@ type FeatureScoreBarProps = {
className?: string;
};

const COLOR_MAP: Record<string, string> = {
積極性: 'bg-orange-500',
冷静さ: 'bg-yellow-400',
論理性: 'bg-blue-500',
慎重さ: 'bg-red-500',
協調性: 'bg-green-500',

positivity: 'bg-orange-500',
calmness: 'bg-yellow-400',
logic: 'bg-blue-500',
caution: 'bg-red-500',
cooperativeness: 'bg-green-500',
};

export default function FeatureScoreBar({
name,
score,
Expand All @@ -15,17 +29,22 @@ export default function FeatureScoreBar({
}: FeatureScoreBarProps) {
const percent = Math.min(100, Math.max(0, (score / max) * 100));

const barColor = COLOR_MAP[name] || 'bg-gray-400';

return (
<div className={className}>
<div className="flex items-center justify-between gap-2">
<span className="text-sm font-medium text-gray-700">{name}</span>
<span className="text-sm text-gray-600">{score}</span>
<div className={`${className} border-2 border-black`}>
<div className="flex items-center justify-between gap-2 mb-1">
<span className="text-sm font-black text-black">{name}</span>
<span className="text-sm font-black text-black">{score}</span>
</div>
<div className="mt-1 h-4 overflow-hidden rounded-full bg-gray-200">

<div className="h-5 w-full overflow-hidden rounded-full border-2 border-black bg-gray-200 relative">
<div
className="h-full rounded-full bg-blue-500 transition-all duration-300"
className={`h-full rounded-r-full ${barColor} transition-all duration-1000 ease-out border-r-2 border-black`}
style={{ width: `${percent}%` }}
/>

<div className="absolute inset-0 opacity-10 pointer-events-none bg-[repeating-linear-gradient(45deg,transparent,transparent_5px,white_5px,white_10px)]" />
</div>
</div>
);
Expand Down
75 changes: 57 additions & 18 deletions frontend/src/features/result/components/GameDetailTab.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
'use client';

import { Users } from 'lucide-react';
import type { GameDetail } from '../types';
import FeatureScoreBar from './FeatureScoreBar';
import MetricsBarChart from './MetricsBarChart';
Expand All @@ -16,43 +17,81 @@ export default function GameDetailTab({
tabColor = '#3b82f6',
}: GameDetailTabProps) {
return (
<div className="grid gap-4 md:grid-cols-2">
<div className="space-y-4">
<section>
<h3 className="mb-2 text-sm font-bold text-gray-700">
<div className="flex flex-col md:flex-row gap-6 lg:gap-8 h-full animate-in slide-in-from-right duration-500">
<div className="w-full md:w-1/3 flex flex-col gap-6">
<section className="bg-blue-50 border-2 border-blue-100 p-3 rounded-[1.5rem] shadow-sm">
<h3 className="mb-2 text-[10px] font-black text-blue-600 flex items-center gap-1 opacity-70">
<Users className="w-3 h-3" />
計測された特徴
</h3>
<div className="space-y-2">
{/* space-y-3 -> space-y-1.5 にしてバーの間隔を詰めました */}
<div className="space-y-1.5">
{detail.feature_scores.map((fs) => (
<FeatureScoreBar
key={fs.axis}
name={fs.name}
score={fs.score}
className="rounded border border-gray-100 bg-white p-2"
// よりコンパクトなスタイルへ
className="bg-white/40 px-2 py-1 rounded-md border border-blue-50/50"
/>
))}
</div>
</section>
<section>
<h3 className="mb-2 text-sm font-bold text-gray-700">解析コメント</h3>
<p className="rounded border border-gray-100 bg-white p-3 text-sm text-gray-700">
{comment}
</p>

<section className="relative bg-white border-4 border-black rounded-[2rem] shadow-[6px_6px_0px_0px_rgba(0,0,0,1)] flex-1 overflow-hidden min-h-0">
<div className="h-full overflow-y-auto custom-scrollbar p-5">
<h3 className="mb-2 text-lg font-black text-black sticky top-0 bg-white py-1 z-10 border-b-2 border-dashed border-gray-100">
解析コメント
</h3>
<div className="text-base font-bold leading-relaxed text-gray-800">
<p className="whitespace-pre-wrap">
{comment
.split(/(\d+\.?\d*秒|\d+回ホバー|\d+回反論)/)
.map((part, i) =>
/(\d+\.?\d*秒|\d+回ホバー|\d+回反論)/.test(part) ? (
// ハイライトの色をタブの色に合わせて動的に変更
<span
key={i}
style={{ backgroundColor: `${tabColor}40` }}
className="px-1 rounded-sm mx-0.5"
>
{part}
</span>
) : (
part
)
)}
</p>
</div>
</div>
</section>
</div>
<section>
<h3 className="mb-2 text-sm font-bold text-gray-700">

<section className="w-full md:w-2/3 flex flex-col pt-2">
<h3 className="text-center font-black text-xl text-gray-400 mb-6 tracking-widest uppercase">
行動ログ詳細データ
</h3>
<div className="rounded border border-gray-100 bg-white p-2 sm:p-3">

<div className="flex-1 min-h-87.5">
<MetricsBarChart
metrics={detail.metrics}
userBarColor={tabColor}
averageBarColor="#9ca3af"
averageBarColor="#d1d5db"
/>
<div className="mt-2 flex gap-4 text-xs text-gray-500">
<span style={{ color: tabColor }}>■ あなた</span>
<span className="text-gray-400">■ 平均</span>
</div>

{/* 凡例 */}
<div className="mt-6 flex justify-center gap-10 font-black text-sm">
<div className="flex items-center gap-3">
<div
className="w-8 h-4 rounded-sm border-2 border-black shadow-[2px_2px_0px_0px_rgba(0,0,0,1)]"
style={{ backgroundColor: tabColor }}
/>
<span style={{ color: tabColor }}>あなた</span>
</div>
<div className="flex items-center gap-3">
<div className="w-8 h-4 rounded-sm bg-gray-300" />
<span className="text-gray-400">平均</span>
</div>
</div>
</section>
Expand Down
81 changes: 45 additions & 36 deletions frontend/src/features/result/components/MetricsBarChart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
Tooltip,
XAxis,
YAxis,
CartesianGrid,
} from 'recharts';
import type { Metric } from '../types';

Expand All @@ -20,58 +21,66 @@ type MetricsBarChartProps = {

export default function MetricsBarChart({
metrics,
userBarColor = '#ef4444',
averageBarColor = '#9ca3af',
userBarColor = '#eab308', // スクショの黄金色
averageBarColor = '#d1d5db',
className = '',
}: MetricsBarChartProps) {
const allValues = metrics.flatMap((m) => [m.user, m.average]);
const minVal = Math.min(...allValues, 0);
const maxVal = Math.max(...allValues, 1);

const data = metrics.map((m) => ({
label: m.label,
user: m.user,
average: m.average,
}));

return (
<div className={className}>
<ResponsiveContainer
width="100%"
height={Math.max(180, metrics.length * 36)}
>
<div className={`${className} w-full h-full`}>
<ResponsiveContainer width="100%" height="100%">
<BarChart
data={data}
data={metrics}
layout="vertical"
margin={{ top: 4, right: 4, left: 0, bottom: 4 }}
margin={{ top: 10, right: 40, left: 40, bottom: 20 }}
barGap={2} // バー同士の隙間を詰める
>
<XAxis type="number" domain={[minVal, maxVal]} hide />
{/* 縦の点線:グリッド */}
<CartesianGrid
strokeDasharray="3 3"
horizontal={false}
stroke="#e5e7eb"
/>

<XAxis
type="number"
axisLine={false}
tickLine={false}
tick={{ fill: '#9ca3af', fontSize: 12 }}
/>
<YAxis
type="category"
dataKey="label"
width={100}
tick={{ fontSize: 10 }}
width={120}
axisLine={false}
tickLine={false}
tick={{ fill: '#000', fontSize: 13, fontWeight: 900 }}
/>

<Tooltip
content={({ active, payload }) => {
if (!active || !payload?.length) return null;
const d = payload[0].payload;
return (
<div className="rounded-lg border border-gray-200 bg-white px-3 py-2 shadow-md">
<p className="font-medium text-gray-800">{d.label}</p>
<p className="text-sm">あなた: {d.user}</p>
<p className="text-sm">平均: {d.average}</p>
</div>
);
cursor={{ fill: '#f1f5f9', opacity: 0.5 }}
contentStyle={{
borderRadius: '12px',
border: '4px solid #000',
fontWeight: '900',
}}
/>
<Bar dataKey="user" name="あなた" barSize={14} radius={[0, 4, 4, 0]}>
{data.map((_, i) => (

{/* あなた:太くて黒枠があるメインバー */}
<Bar
dataKey="user"
name="あなた"
barSize={24}
radius={[0, 10, 10, 0]}
stroke="#000"
strokeWidth={2.5}
>
{metrics.map((_, i) => (
<Cell key={`user-${i}`} fill={userBarColor} />
))}
</Bar>
<Bar dataKey="average" name="平均" barSize={14} radius={[0, 4, 4, 0]}>
{data.map((_, i) => (

<Bar dataKey="average" name="平均" barSize={12} radius={[0, 6, 6, 0]}>
{metrics.map((_, i) => (
<Cell key={`avg-${i}`} fill={averageBarColor} />
))}
</Bar>
Expand Down
Loading
Loading